Cypher発行SQL関数

雨が降ってバイクで遊びに行けない(´・ω・`)
まあ、いい機会だからPostgreSQL 9.3 beta1のJSON演算子を検証してみるのもいいか。

PostgreSQL 9.3で強化されたJSON

9.2の時点では格納(の背景でのパース)と取り出し程度しか機能がなく、ビミョーな感じだったJSON型だけど、9.3では演算子や各種関数がかなり強化されたようだ。
PostgreSQL: Documentation: 9.3: JSON Functions and Operators

昨日、9.3betaをインストールしていろいろ試す環境はできたので、JSON型の操作等をいろいろ試してみようか。

まずは基本的なキーからの値の取得。

json=# SELECT '{"a":1,"b":2}'::json->'b';
 ?column? 
----------
 2
(1 row)

json=# 

なるほど、こんな感じなのか。

Cypher発行SQL関数

さて、本格的に検証する前に、色んなパターンのJSONデータを作成する環境もつくっておきたい。
そして手元にはグラフデータベースNeo4jがあるじゃなイカ
Neo4jにはREST APIがあり、さらにNeo4jの問合わせ言語CypherもREST APIで投げることができる。
そのレスポンスもJSONなので、それをそのまま返却するSQL関数を作成すれば、Neo4j上の結果をPostgreSQLJSON演算子で扱うことができるはず。
幸い、先日作成したneo4j_fdw(neo4j_fdw プロトタイプ - 日々の記録 別館参照)の内部関数を呼び出せば、数行程度でこのSQL関数は実装できる。
ということで、さくっと実装してデータベースに組み込んでみた。

json=# \dx
                         List of installed extensions
   Name    | Version |   Schema   |                Description                
-----------+---------+------------+-------------------------------------------
 neo4j_fdw | 1.0     | public     | foreign-data wrapper for flat file access
 plpgsql   | 1.0     | pg_catalog | PL/pgSQL procedural language
(2 rows)

json=# \dx+ neo4j_fdw
     Objects in extension "neo4j_fdw"
            Object Description            
------------------------------------------
 foreign-data wrapper neo4j_fdw
 function exec_cypher(text,text)
 function neo4j_fdw_handler()
 function neo4j_fdw_validator(text[],oid)
(4 rows)

json=# 

exec_cypherがその関数だ。
さっそく動かしてみる。

json=# SELECT exec_cypher('http://localhost:7474/db/data/cypher', '{"query" : "START n=node(*) WHERE n.gender? = \"Famale\" RETURN n.name as name, n.gender? as gender"}');
                                                                           exec_cypher                                                                           
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
 {                                                                                                                                                              +
   "columns" : [ "name", "gender" ],                                                                                                                            +
   "data" : [ [ "伊勢", null ], [ "日向", null ], [ "赤城", "Famale" ], [ "加賀", "Famale" ], [ "信濃", "Famale" ], [ "飛龍", "Famale" ], [ "蒼龍", "Famale" ] ]+
 }
(1 row)

json=#

お、一応JSONらしきものが返ってきた。

ちょっと凝ったCypherクエリを発行してみる。
これは両思い(お互いにフォローしているユーザ)を検索するものだ。

json=# SELECT exec_cypher('http://localhost:7474/db/data/cypher', '{"query":"START n=node(*) MATCH p=fm<-[:follow]-n<-[:follow]-fm  RETURN n.name as my_name, n.gender as my_gender, fm.name  as follower_name, fm.gender as follower_gender" }');
                                                                                                                                                                                           
                                       exec_cypher                                                                                                                                         
                                                                                         
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------
 {                                                                                                                                                                                         
                                                                                                                                                                                           
                                                                                        +
   "columns" : [ "my_name", "my_gender", "follower_name", "follower_gender" ],                                                                                                             
                                                                                                                                                                                           
                                                                                        +
   "data" : [ [ "大和", "Male", "武蔵", "Male" ], [ "大和", "Male", "信濃", "Famale" ], [ "大和", "Male", "長門", "Male" ], [ "武蔵", "Male", "信濃", "Famale" ], [ "武蔵", "Male", "大和",
 "Male" ], [ "長門", "Male", "陸奥", "Male" ], [ "長門", "Male", "大和", "Male" ], [ "陸奥", "Male", "長門", "Male" ], [ "赤城", "Famale", "加賀", "Famale" ], [ "加賀", "Famale", "赤城", 
"Famale" ], [ "信濃", "Famale", "武蔵", "Male" ], [ "信濃", "Famale", "大和", "Male" ] ]+
 }
(1 row)

json=#

例えば、このJSON結果から"->"演算子を使ってdataをキーとする結果を抜き出してみる。

json=# SELECT exec_cypher('http://localhost:7474/db/data/cypher', '{"query":"START n=node(*) MATCH p=fm<-[:follow]-n<-[:follow]-fm  RETURN n.name as my_name, n.gender as my_gender, fm.name  as follower_name, fm.gender as follower_gender" }')->'data';
                                                                                                                                                                                          
                                    ?column?                                                                                                                                              
                                                                                
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
 [ [ "大和", "Male", "武蔵", "Male" ], [ "大和", "Male", "信濃", "Famale" ], [ "大和", "Male", "長門", "Male" ], [ "武蔵", "Male", "信濃", "Famale" ], [ "武蔵", "Male", "大和", "Male" ],
 [ "長門", "Male", "陸奥", "Male" ], [ "長門", "Male", "大和", "Male" ], [ "陸奥", "Male", "長門", "Male" ], [ "赤城", "Famale", "加賀", "Famale" ], [ "加賀", "Famale", "赤城", "Famale" 
], [ "信濃", "Famale", "武蔵", "Male" ], [ "信濃", "Famale", "大和", "Male" ] ]
(1 row)

json=#

この結果から、さらに2番めのエントリを抜き出す(0相対なので1を指定する)。

json=# SELECT exec_cypher('http://localhost:7474/db/data/cypher', '{"query":"START n=node(*) MATCH p=fm<-[:follow]-n<-[:follow]-fm  RETURN n.name as my_name, n.gender as my_gender, fm.name  as follower_name, fm.gender as follower_gender" }')->'data'->1;
               ?column?               
--------------------------------------
 [ "大和", "Male", "信濃", "Famale" ]
(1 row)

json=#

ちなみに、CypherのRETURN句でノード全体を返却すると、以下の様な結構複雑なJSONが返却される。

json=# SELECT exec_cypher('http://localhost:7474/db/data/cypher', '{"query":"START n=node(*) RETURN n" }');
                                                   exec_cypher                                                    
------------------------------------------------------------------------------------------------------------------
 {                                                                                                               +
   "columns" : [ "n" ],                                                                                          +
   "data" : [ [ {                                                                                                +
     "paged_traverse" : "http://localhost:7474/db/data/node/1/paged/traverse/{returnType}{?pageSize,leaseTime}", +
     "outgoing_relationships" : "http://localhost:7474/db/data/node/1/relationships/out",                        +
     "data" : {                                                                                                  +
       "location" : "奈良県",                                                                                    +
       "description" : "でかーい。俺の46サンチ砲が火を吹くぜ",                                                   +
       "name" : "大和",                                                                                          +
       "gender" : "Male"                                                                                         +
     },                                                                                                          +
     "traverse" : "http://localhost:7474/db/data/node/1/traverse/{returnType}",                                  +
     "all_typed_relationships" : "http://localhost:7474/db/data/node/1/relationships/all/{-list|&|types}",       +
     "all_relationships" : "http://localhost:7474/db/data/node/1/relationships/all",                             +
     "property" : "http://localhost:7474/db/data/node/1/properties/{key}",                                       +
     "self" : "http://localhost:7474/db/data/node/1",                                                            +
     "outgoing_typed_relationships" : "http://localhost:7474/db/data/node/1/relationships/out/{-list|&|types}",  +
     "properties" : "http://localhost:7474/db/data/node/1/properties",                                           +
     "incoming_relationships" : "http://localhost:7474/db/data/node/1/relationships/in",                         +
     "incoming_typed_relationships" : "http://localhost:7474/db/data/node/1/relationships/in/{-list|&|types}",   +
     "extensions" : {                                                                                            +
     },                                                                                                          +
     "create_relationship" : "http://localhost:7474/db/data/node/1/relationships"                                +
   } ], [ {                                                                                                      +
     "paged_traverse" : "http://localhost:7474/db/data/node/2/paged/traverse/{returnType}{?pageSize,leaseTime}", +
     "outgoing_relationships" : "http://localhost:7474/db/data/node/2/relationships/out",                        +
     "data" : {                                                                                                  +
       "location" : "東京都",                                                                                    +
       "description" : "影が薄いです",                                                                           +
       "name" : "武蔵",                                                                                          +
       "gender" : "Male"                                                                                         +
     },                                                                                                          +
     "traverse" : "http://localhost:7474/db/data/node/2/traverse/{returnType}",                                  +
     "all_typed_relationships" : "http://localhost:7474/db/data/node/2/relationships/all/{-list|&|types}",       +
     "all_relationships" : "http://localhost:7474/db/data/node/2/relationships/all",                             +
     "property" : "http://localhost:7474/db/data/node/2/properties/{key}",                                       +
     "self" : "http://localhost:7474/db/data/node/2",                                                            +
     "outgoing_typed_relationships" : "http://localhost:7474/db/data/node/2/relationships/out/{-list|&|types}",  +
     "properties" : "http://localhost:7474/db/data/node/2/properties",                                           +
     "incoming_relationships" : "http://localhost:7474/db/data/node/2/relationships/in",                         +
     "incoming_typed_relationships" : "http://localhost:7474/db/data/node/2/relationships/in/{-list|&|types}",   +
     "extensions" : {                                                                                            +
     },                                                                                                          +
     "create_relationship" : "http://localhost:7474/db/data/node/2/relationships"                                +
   } ], [ {                                                                                                      +
(以下略)

これを元ネタにしてJSON演算子を検証してみるのも良さそうだ。