Skip to content

Commit

Permalink
Merge pull request #11 from tminglei/pgjson
Browse files Browse the repository at this point in the history
Pgjson
  • Loading branch information
tminglei committed Sep 29, 2013
2 parents 099c8fa + 1504fed commit a3a65d8
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 9 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ Install
-------
To use `slick-pg` in [sbt](http://www.scala-sbt.org/ "slick-sbt") project, add the following to your project file:
```scala
libraryDependencies += "com.github.tminglei" % "slick-pg_2.10.1" % "0.1.3.1"
libraryDependencies += "com.github.tminglei" % "slick-pg_2.10.1" % "0.1.5"
```

Or, in [maven](http://maven.apache.org/ "maven") project, you can add `slick-pg` to your `pom.xml` like this:
```xml
<dependency>
<groupId>com.github.tminglei</groupId>
<artifactId>slick-pg_2.10.1</artifactId>
<version>0.1.3.1</version>
<version>0.1.5</version>
</dependency>
```

Expand Down Expand Up @@ -121,6 +121,7 @@ val db = Database.forURL(url = "jdbc:postgresql://localhost/test?user=test", dri
Data types/operators/functions
------------------------------
- [Array's operators/functions](https://github.com/tminglei/slick-pg/tree/master/src/main/scala/com/github/tminglei.slickpg#array "Array's operators/functions")
- [JSON's operators/functions](https://github.com/tminglei/slick-pg/tree/master/src/main/scala/com/github/tminglei.slickpg#json "JSON's operators/functions")
- [Datetime's operators/functions](https://github.com/tminglei/slick-pg/tree/master/src/main/scala/com/github/tminglei.slickpg#datetime "Datetime's operators/functions")
- [Range's operators/functions](https://github.com/tminglei/slick-pg/tree/master/src/main/scala/com/github/tminglei.slickpg#range "Range's operators/functions")
- [HStore's operators/functions](https://github.com/tminglei/slick-pg/tree/master/src/main/scala/com/github/tminglei.slickpg#hstore "HStore's operators/functions")
Expand All @@ -130,6 +131,9 @@ Data types/operators/functions

Version history
---------------
v0.1.5 (29-Sep-2013):
1) support pg json

v0.1.3.1 (13-Sep-2013):
1) fix Issue #10

Expand Down
5 changes: 3 additions & 2 deletions example/project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ object ExampleBuild extends Build {

libraryDependencies := Seq(
"com.typesafe.slick" % "slick_2.10" % "1.0.1",
"com.github.tminglei" % "slick-pg_2.10.1" % "0.1.3.1",
"org.postgresql" % "postgresql" % "9.2-1003-jdbc4",
"com.vividsolutions" % "jts" % "1.13",
"postgresql" % "postgresql" % "9.1-901.jdbc4",
"com.github.tminglei" % "slick-pg_2.10.1" % "0.1.1"
"org.json4s" % "json4s-native_2.10" % "3.2.5"
),
resolvers += Resolver.mavenLocal,
resolvers += Resolver.sonatypeRepo("snapshots"),
Expand Down
8 changes: 7 additions & 1 deletion example/src/scala/com.example/MyPostgresDriver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ trait MyPostgresDriver extends PostgresDriver
with PgArraySupport
with PgRangeSupport
with PgHStoreSupport
// with PgJsonSupport[text.Document]
with PgSearchSupport
with PostGISSupport {
with PostGISSupport
with PgDatetimeSupport {

// override val jsonMethods = org.json4s.native.JsonMethods

override val Implicit = new ImplicitsPlus {}
override val simple = new SimpleQLPlus {}
Expand All @@ -18,8 +22,10 @@ trait MyPostgresDriver extends PostgresDriver
with ArrayImplicits
with RangeImplicits
with HStoreImplicits
// with JsonImplicits
with SearchImplicits
with PostGISImplicits
with DatetimeImplicits

trait SimpleQLPlus extends SimpleQL
with ImplicitsPlus
Expand Down
7 changes: 5 additions & 2 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ object SlickPgBuild extends Build {
lazy val theSettings = Seq(
name := "slick-pg",
description := "Slick extensions for PostgreSQL",
version := "0.1.3.1",
version := "0.1.5",
organizationName := "slick-pg",
organization := "com.github.tminglei",

Expand All @@ -18,8 +18,11 @@ object SlickPgBuild extends Build {
"-language:postfixOps"),
libraryDependencies := Seq(
"com.typesafe.slick" % "slick_2.10" % "1.0.0",
"com.vividsolutions" % "jts" % "1.13",
"org.postgresql" % "postgresql" % "9.2-1003-jdbc4",
"com.vividsolutions" % "jts" % "1.13",
"org.json4s" % "json4s-ast_2.10" % "3.2.5",
"org.json4s" % "json4s-core_2.10" % "3.2.5",
"org.json4s" % "json4s-native_2.10" % "3.2.5" % "test",
"junit" % "junit" % "4.11" % "test",
"com.novocode" % "junit-interface" % "0.10" % "test"
),
Expand Down
106 changes: 106 additions & 0 deletions src/main/scala/com/github/tminglei.slickpg/PgJsonSupport.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.github.tminglei.slickpg

import scala.slick.driver.{BasicProfile, PostgresDriver}
import scala.slick.ast.Library.{SqlFunction, SqlOperator}
import scala.slick.lifted._
import scala.slick.session.{PositionedResult, PositionedParameters}
import org.postgresql.util.PGobject
import scala.slick.ast.Node
import org.json4s._

trait PgJsonSupport[T] { driver: PostgresDriver =>

val jsonMethods: JsonMethods[T]

trait JsonImplicits {
implicit val jsonTypeMapper = new JsonTypeMapper(jsonMethods)

implicit def jsonColumnExtensionMethods(c: Column[JValue])(
implicit tm: TypeMapper[JValue], tm1: TypeMapper[List[String]]) = {
new JsonColumnExtensionMethods[JValue](c)
}
implicit def jsonOptionColumnExtensionMethods(c: Column[Option[JValue]])(
implicit tm: TypeMapper[JValue], tm1: TypeMapper[List[String]]) = {
new JsonColumnExtensionMethods[Option[JValue]](c)
}
}

/////////////////////////////////////////////////////////////////

object JsonLibrary {
val -> = new SqlOperator("->")
val ->> = new SqlOperator("->>")
val #> = new SqlOperator("#>")
val #>> = new SqlOperator("#>>")

val arrayLength = new SqlFunction("json_array_length")
val arrayElements = new SqlFunction("json_array_elements")
val objectKeys = new SqlFunction("json_object_keys")
// val rowToJson = new SqlFunction("row_to_json") //not support, since "row" type not supported by slick/slick-pg yet
// val jsonEach = new SqlFunction("json_each") //not support, since "row" type not supported by slick/slick-pg yet
// val jsonEachText = new SqlFunction("json_each_text") //not support, since "row" type not supported by slick/slick-pg yet
// val jsonPopulateRecord = new SqlFunction("json_populate_record") //not support, since "row" type not supported by slick/slick-pg yet
// val jsonPopulateRecordset = new SqlFunction("json_populate_recordset") //not support, since "row" type not supported by slick/slick-pg yet
}

class JsonColumnExtensionMethods[P1](val c: Column[P1])(
implicit tm: TypeMapper[JValue], tm1: TypeMapper[List[String]])
extends ExtensionMethods[JValue, P1] {
/** Note: json array's index starts with 0 */
def ~> [P2, R](index: Column[P2])(implicit om: o#arg[Int, P2]#to[JValue, R]) = {
om(JsonLibrary.->.column[JValue](n, Node(index)))
}
def ~>>[P2, R](index: Column[P2])(implicit om: o#arg[Int, P2]#to[String, R]) = {
om(JsonLibrary.->>.column[String](n, Node(index)))
}
def +> [P2, R](key: Column[P2])(implicit om: o#arg[String, P2]#to[JValue, R]) = {
om(JsonLibrary.->.column[JValue](n, Node(key)))
}
def +>>[P2, R](key: Column[P2])(implicit om: o#arg[String, P2]#to[String, R]) = {
om(JsonLibrary.->>.column[String](n, Node(key)))
}
def #> [P2, R](keyPath: Column[P2])(implicit om: o#arg[List[String], P2]#to[JValue, R]) = {
om(JsonLibrary.#>.column[JValue](n, Node(keyPath)))
}
def #>>[P2, R](keyPath: Column[P2])(implicit om: o#arg[List[String], P2]#to[String, R]) = {
om(JsonLibrary.#>>.column[String](n, Node(keyPath)))
}

def arrayLength[R](implicit om: o#to[Int, R]) = om(JsonLibrary.arrayLength.column(n))
def arrayElements[R](implicit om: o#to[JValue, R]) = om(JsonLibrary.arrayElements.column(n))
def objectKeys[R](implicit om: o#to[String, R]) = om(JsonLibrary.objectKeys.column(n))
}

//////////////////////////////////////////////////////////////////

class JsonTypeMapper(jsonMethods: JsonMethods[T]) extends TypeMapperDelegate[JValue] with BaseTypeMapper[JValue] {
import jsonMethods._

def apply(v1: BasicProfile): TypeMapperDelegate[JValue] = this

//----------------------------------------------------------
def zero = null

def sqlType = java.sql.Types.OTHER

def sqlTypeName = "json"

def setValue(v: JValue, p: PositionedParameters) = p.setObject(mkPgObject(v), sqlType)

def setOption(v: Option[JValue], p: PositionedParameters) = p.setObjectOption(v.map(mkPgObject), sqlType)

def nextValue(r: PositionedResult) = r.nextStringOption().map(parse(_)).getOrElse(zero)

def updateValue(v: JValue, r: PositionedResult) = r.updateObject(mkPgObject(v))

override def valueToSQLLiteral(v: JValue) = pretty(render(v))

///
private def mkPgObject(v: JValue) = {
val obj = new PGobject
obj.setType(sqlTypeName)
obj.setValue(compact(render(v)))
obj
}
}
}
16 changes: 14 additions & 2 deletions src/main/scala/com/github/tminglei.slickpg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,22 @@ Supported data type's operators/functions
| + | &#124;&#124; | array-to-element concatenation| ARRAY[4,5,6] &#124;&#124; 7 | {4,5,6,7} |
| +: | &#124;&#124; | element-to-array concatenation| 3 &#124;&#124; ARRAY[4,5,6] | {3,4,5,6} |
| length | array_length | length of the array/dimension | array_length(array[1,2,3], 1) | 3 |
| unnest | unnest | expand array to a set of rows | unnest(ARRAY[1,2]) | 1<br/>2<br/>(2 rows) |
| unnest | unnest | expand array to a set of rows | unnest(ARRAY[1,2]) | 1<br/> 2<br/> (2 rows) |

#### JSON
| Slick Operator/Function | PG Operator/Function | Description | Example | Result |
| ----------------------- | -------------------- | ----------------------------- | ------------------------------- | ------ |
| ~> | -> | Get JSON array element | '[1,2,3]'::json->2 | 3 |
| ~>> | ->> | Get JSON array element as text| '[1,2,3]'::json->>2 | "3" |
| +> | -> | Get JSON object field | '{"a":1,"b":2}'::json->'b' | 2 |
| +>> | ->> | Get JSON object field as text | '{"a":1,"b":2}'::json->>'b' | "2" |
| #> | #> | Get JSON object at specified path | '{"a":[1,2,3],"b":[4,5,6]}'::json#>'{a,2}' | 3 |
| #>> | #>> | Get JSON object at specified path as text | '{"a":[1,2,3],"b":[4,5,6]}'::json#>>'{a,2}' | "3" |
| arrayLength | json_array_length | Returns elem number of outermost JSON array | json_array_length('[1,2,3,{"f1":1,"f2":[5,6]},4]') | 5 |
| arrayElements | json_array_elements | Expands JSON array to set of JSON elements | json_array_elements('[1,true, [2,false]]') | value<br/> -------------<br/> 1<br/> true<br/> [2,false] |
| objectKeys | json_object_keys | Returns set of keys in outermost JSON object | json_object_keys('{"f1":"abc","f2":{"f3":"a", "f4":"b"}}') | json_object_keys<br/> ----------------<br/> f1<br/> f2 |

### Datetime
#### Datetime
| Slick Operator/Function | PG Operator/Function | Description | Example | Result |
| ----------------------- | -------------------- | ----------------------- | ---------------------------------------------- | ------------------------ |
| +++ | + | timestamp + interval | timestamp '2001-09-28 01:00' + interval '23 hours'|timestamp '2001-09-29 00:00:00'|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ trait MyPostgresDriver extends PostgresDriver
with PgDatetimeSupport
with PgRangeSupport
with PgHStoreSupport
with PgJsonSupport[text.Document]
with PgSearchSupport
with PostGISSupport {

override val jsonMethods = org.json4s.native.JsonMethods

override val Implicit = new ImplicitsPlus {}
override val simple = new SimpleQLPlus {}

Expand All @@ -19,6 +22,7 @@ trait MyPostgresDriver extends PostgresDriver
with DatetimeImplicits
with RangeImplicits
with HStoreImplicits
with JsonImplicits
with SearchImplicits
with PostGISImplicits

Expand Down
99 changes: 99 additions & 0 deletions src/test/scala/com/github/tminglei.slickpg/PgJsonSupportTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.github.tminglei.slickpg

import org.junit._
import org.junit.Assert._
import org.json4s._

class PgJsonSupportTest {
import MyPostgresDriver.simple._
import MyPostgresDriver.jsonMethods._

val db = Database.forURL(url = "jdbc:postgresql://localhost/test?user=test", driver = "org.postgresql.Driver")

case class JsonBean(id: Long, json: JValue)

object JsonTestTable extends Table[JsonBean](Some("test"), "JsonTest") {
def id = column[Long]("id", O.AutoInc, O.PrimaryKey)
def json = column[JValue]("json")

def * = id ~ json <> (JsonBean, JsonBean unapply _)
}

//------------------------------------------------------------------------------

val testRec1 = JsonBean(33L, parse(""" { "a":101, "b":"aaa", "c":[3,4,5,9] } """))
val testRec2 = JsonBean(35L, parse(""" [ {"a":"v1","b":2}, {"a":"v5","b":3} ] """))

@Test
def testJsonFunctions(): Unit = {
db withSession { implicit session: Session =>
JsonTestTable.insert(testRec1)
JsonTestTable.insert(testRec2)

val json1 = parse(""" {"a":"v1","b":2} """)
val json2 = parse(""" {"a":"v5","b":3} """)

val q0 = JsonTestTable.where(_.id === testRec2.id.bind).map(_.json)
println(s"sql0 = ${q0.selectStatement}")
assertEquals(JArray(List(json1,json2)), q0.first())

// pretty(render(JInt(101))) will get "101", but parse("101") will fail, since json string must start with '{' or '['
// val q1 = JsonTestTable.where(_.id === testRec1.id.bind).map(_.json.+>("a"))
// println(s"'+>' sql = ${q1.selectStatement}")
// assertEquals(JInt(101), q1.first())

val q11 = JsonTestTable.where(_.json.+>>("a") === "101".bind).map(_.json.+>>("c"))
println(s"'+>>' sql = ${q11.selectStatement}")
assertEquals("[3,4,5,9]", q11.first())

val q12 = JsonTestTable.where(_.json.+>>("a") === "101".bind).map(_.json.+>("c"))
println(s"'+>' sql = ${q12.selectStatement}")
assertEquals(JArray(List(JInt(3), JInt(4), JInt(5), JInt(9))), q12.first())

// json array's index starts with 0
val q2 = JsonTestTable.where(_.id === testRec2.id).map(_.json.~>(1))
println(s"'~>' sql = ${q2.selectStatement}")
assertEquals(json2, q2.first())

val q21 = JsonTestTable.where(_.id === testRec2.id).map(_.json.~>>(1))
println(s"'~>>' sql = ${q21.selectStatement}")
assertEquals("""{"a":"v5","b":3}""", q21.first())

val q3 = JsonTestTable.where(_.id === testRec2.id).map(_.json.arrayLength)
println(s"'arrayLength' sql = ${q3.selectStatement}")
assertEquals(2, q3.first())

val q4 = JsonTestTable.where(_.id === testRec2.id).map(_.json.arrayElements)
println(s"'arrayElements' sql = ${q4.selectStatement}")
assertEquals(List(json1, json2), q4.list())

val q41 = JsonTestTable.where(_.id === testRec2.id).map(_.json.arrayElements)
println(s"'arrayElements' sql = ${q41.selectStatement}")
assertEquals(json1, q41.first())

val q5 = JsonTestTable.where(_.id === testRec1.id).map(_.json.objectKeys)
println(s"'objectKeys' sql = ${q5.selectStatement}")
assertEquals(List("a","b","c"), q5.list())

val q51 = JsonTestTable.where(_.id === testRec1.id).map(_.json.objectKeys)
println(s"'objectKeys' sql = ${q51.selectStatement}")
assertEquals("a", q51.first())
}
}

//------------------------------------------------------------------------------

@Before
def createTables(): Unit = {
db withSession { implicit session: Session =>
JsonTestTable.ddl create
}
}

@After
def dropTables(): Unit = {
db withSession { implicit session: Session =>
JsonTestTable.ddl drop
}
}
}

0 comments on commit a3a65d8

Please sign in to comment.