From 4c12fa9d17b9d43dece9dd695662a8db8bfe2334 Mon Sep 17 00:00:00 2001 From: Tu_Minglei Date: Tue, 17 Sep 2013 08:59:50 +0800 Subject: [PATCH 1/4] adding pgjson support --- example/project/Build.scala | 5 +- .../scala/com.example/MyPostgresDriver.scala | 8 +- project/Build.scala | 5 +- .../tminglei.slickpg/PgJsonSupport.scala | 106 ++++++++++++++++++ .../tminglei.slickpg/MyPostgresDriver.scala | 4 + .../tminglei.slickpg/PgJsonSupportTest.scala | 98 ++++++++++++++++ 6 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 src/main/scala/com/github/tminglei.slickpg/PgJsonSupport.scala create mode 100644 src/test/scala/com/github/tminglei.slickpg/PgJsonSupportTest.scala diff --git a/example/project/Build.scala b/example/project/Build.scala index bcb8b79e..e509a636 100644 --- a/example/project/Build.scala +++ b/example/project/Build.scala @@ -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"), diff --git a/example/src/scala/com.example/MyPostgresDriver.scala b/example/src/scala/com.example/MyPostgresDriver.scala index 85e3ccde..0b36933f 100644 --- a/example/src/scala/com.example/MyPostgresDriver.scala +++ b/example/src/scala/com.example/MyPostgresDriver.scala @@ -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 {} @@ -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 diff --git a/project/Build.scala b/project/Build.scala index 9ed8bf2a..4cf5753e 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -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" ), diff --git a/src/main/scala/com/github/tminglei.slickpg/PgJsonSupport.scala b/src/main/scala/com/github/tminglei.slickpg/PgJsonSupport.scala new file mode 100644 index 00000000..f97f47fc --- /dev/null +++ b/src/main/scala/com/github/tminglei.slickpg/PgJsonSupport.scala @@ -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] { + + 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 + } + } +} diff --git a/src/test/scala/com/github/tminglei.slickpg/MyPostgresDriver.scala b/src/test/scala/com/github/tminglei.slickpg/MyPostgresDriver.scala index 942c2bda..dfddc5d2 100644 --- a/src/test/scala/com/github/tminglei.slickpg/MyPostgresDriver.scala +++ b/src/test/scala/com/github/tminglei.slickpg/MyPostgresDriver.scala @@ -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 {} @@ -19,6 +22,7 @@ trait MyPostgresDriver extends PostgresDriver with DatetimeImplicits with RangeImplicits with HStoreImplicits + with JsonImplicits with SearchImplicits with PostGISImplicits diff --git a/src/test/scala/com/github/tminglei.slickpg/PgJsonSupportTest.scala b/src/test/scala/com/github/tminglei.slickpg/PgJsonSupportTest.scala new file mode 100644 index 00000000..dcbdee13 --- /dev/null +++ b/src/test/scala/com/github/tminglei.slickpg/PgJsonSupportTest.scala @@ -0,0 +1,98 @@ +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 json3 = parse("101") + + val q0 = JsonTestTable.where(_.id === testRec2.id.bind).map(_.json) + println(s"sql0 = ${q0.selectStatement}") + assertEquals(JArray(List(json1,json2)), q0.first()) + + 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()) + + val q2 = JsonTestTable.where(_.id === testRec2.id).map(_.json.~>(1)) + println(s"'~>' sql = ${q2.selectStatement}") + assertEquals(json1, q2.first()) + + val q21 = JsonTestTable.where(_.id === testRec2.id).map(_.json.~>>(2)) + 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 + } + } +} From ab2b8f228029992daec6a7d0f6f4062e038a9a09 Mon Sep 17 00:00:00 2001 From: Tu_Minglei Date: Tue, 17 Sep 2013 23:52:34 +0800 Subject: [PATCH 2/4] added test for pgjson support --- .../com/github/tminglei.slickpg/PgJsonSupport.scala | 2 +- .../github/tminglei.slickpg/PgJsonSupportTest.scala | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/scala/com/github/tminglei.slickpg/PgJsonSupport.scala b/src/main/scala/com/github/tminglei.slickpg/PgJsonSupport.scala index f97f47fc..c0824779 100644 --- a/src/main/scala/com/github/tminglei.slickpg/PgJsonSupport.scala +++ b/src/main/scala/com/github/tminglei.slickpg/PgJsonSupport.scala @@ -46,7 +46,7 @@ trait PgJsonSupport[T] { driver: PostgresDriver => 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))) } diff --git a/src/test/scala/com/github/tminglei.slickpg/PgJsonSupportTest.scala b/src/test/scala/com/github/tminglei.slickpg/PgJsonSupportTest.scala index dcbdee13..f5ea68f1 100644 --- a/src/test/scala/com/github/tminglei.slickpg/PgJsonSupportTest.scala +++ b/src/test/scala/com/github/tminglei.slickpg/PgJsonSupportTest.scala @@ -32,15 +32,15 @@ class PgJsonSupportTest { val json1 = parse(""" {"a":"v1","b":2} """) val json2 = parse(""" {"a":"v5","b":3} """) - val json3 = parse("101") val q0 = JsonTestTable.where(_.id === testRec2.id.bind).map(_.json) println(s"sql0 = ${q0.selectStatement}") assertEquals(JArray(List(json1,json2)), q0.first()) - val q1 = JsonTestTable.where(_.id === testRec1.id.bind).map(_.json.+>("a")) - println(s"'+>' sql = ${q1.selectStatement}") - assertEquals(JInt(101), q1.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}") @@ -50,11 +50,12 @@ class PgJsonSupportTest { 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(json1, q2.first()) + assertEquals(json2, q2.first()) - val q21 = JsonTestTable.where(_.id === testRec2.id).map(_.json.~>>(2)) + val q21 = JsonTestTable.where(_.id === testRec2.id).map(_.json.~>>(1)) println(s"'~>>' sql = ${q21.selectStatement}") assertEquals("""{"a":"v5","b":3}""", q21.first()) From 4a0191cf37200d735679889aa3e99d1d782dc6e1 Mon Sep 17 00:00:00 2001 From: tminglei Date: Sun, 29 Sep 2013 22:18:09 +0800 Subject: [PATCH 3/4] add pg json support (doc) --- README.md | 8 ++++++-- .../scala/com/github/tminglei.slickpg/README.md | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5458cdfc..6ddaf5b6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ 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: @@ -24,7 +24,7 @@ Or, in [maven](http://maven.apache.org/ "maven") project, you can add `slick-pg` com.github.tminglei slick-pg_2.10.1 - 0.1.3.1 + 0.1.5 ``` @@ -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") @@ -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 diff --git a/src/main/scala/com/github/tminglei.slickpg/README.md b/src/main/scala/com/github/tminglei.slickpg/README.md index 7fe9874f..b1e431c0 100644 --- a/src/main/scala/com/github/tminglei.slickpg/README.md +++ b/src/main/scala/com/github/tminglei.slickpg/README.md @@ -13,10 +13,22 @@ Supported data type's operators/functions | + | || | array-to-element concatenation| ARRAY[4,5,6] || 7 | {4,5,6,7} | | +: | || | element-to-array concatenation| 3 || 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
2
(2 rows) | +| unnest | unnest | expand array to a set of rows | unnest(ARRAY[1,2]) | 1
2
(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
-------------
1
true
[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
----------------
f1
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'| From 1504fedeb139fab783b5151f03ad13045878e132 Mon Sep 17 00:00:00 2001 From: tminglei Date: Sun, 29 Sep 2013 22:18:23 +0800 Subject: [PATCH 4/4] jump to v0.1.5 --- project/Build.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Build.scala b/project/Build.scala index 4cf5753e..fd0347d0 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -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",