Skip to content

Commit 3ba7140

Browse files
committed
Closes #2 - initial implementation of direct cypher queries
1 parent a3548ea commit 3ba7140

File tree

6 files changed

+115
-10
lines changed

6 files changed

+115
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ pom.xml.asc
1313
.nrepl-port
1414
.cpcache/
1515
.calva
16+
.metals

deps.edn

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{:deps {bbenzikry/neo4clj {:git/url "https://github.com/bbenzikry/neo4clj" :sha "f7fa0a27a56797e646458de6a4efeffb3381294d"}
2+
neo4j/neo4j-bi-jdbc {:mvn/version "1.0.0"}
3+
org.neo4j/neo4j-cypher {:mvn/version "4.1.1"}}}

project.clj

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
1-
(defproject metabase/neo4j-driver "0.0.2-SNAPSHOT-neo4j-connector-1.0.0"
1+
(defproject metabase/neo4j-driver "0.0.3-SNAPSHOT-neo4j-connector-1.0.0"
22
:min-lein-version "2.5.0"
3-
4-
; :repositories [["bintray" "https://dl.bintray.com/meetr/thirdparty"]]
5-
6-
:dependencies
7-
[[neo4j/neo4j-bi-jdbc "1.0.0"]]
8-
3+
; git repo support
4+
:plugins [[lein-tools-deps "0.4.5"]]
5+
:middleware [lein-tools-deps.plugin/resolve-dependencies-with-deps-edn]
6+
:lein-tools-deps/config {:config-files [:install :user :project]}
97
:aliases
108
{"test" ["with-profile" "+unit_tests" "test"]}
119

1210
:profiles
1311
{:provided
1412
{:dependencies
1513
[[metabase-core "1.0.0-SNAPSHOT"]]}
16-
1714
:uberjar
1815
{:auto-clean true
1916
:aot :all

src/metabase/driver/neo4j.clj

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
(ns metabase.driver.neo4j
22
"Metabase Neo4j Driver."
3+
(:import [org.neo4j.cypher.internal.parser CypherParser]
4+
[org.neo4j.cypher.internal.util OpenCypherExceptionFactory])
35
(:require [clojure
46
[set :as set]]
7+
[clojure.tools.logging :as log]
58
[honeysql.core :as hsql]
69
[metabase.util
710
[honeysql-extensions :as hx]]
@@ -20,6 +23,7 @@
2023
(defn- make-subname [host port db jdbc-flags]
2124
(str "//" host ":" port "/" db jdbc-flags))
2225

26+
; Support multiple :classname
2327
(defn neo4j
2428
"Create a Clojure JDBC database specification for Neo4j."
2529
[{:keys [host port db jdbc-flags]
@@ -137,6 +141,21 @@
137141
[driver database table]
138142
(sql-jdbc.sync/describe-table driver database table))
139143

144+
145+
146+
; | Execution + Cypher support |
147+
; | TODO: Connection pooling |
148+
; +----------------------------------------------------------------------------------------------------------------+
149+
150+
151+
(def ^:private cypher-parser (CypherParser.))
152+
(def ^:private cypher-exception-factory (OpenCypherExceptionFactory. nil))
153+
(defn- cypher? [{{query :query} :native}]
154+
(try
155+
(log/info "Received neo4j query. Checking if cypher ❓")
156+
(.parse cypher-parser query cypher-exception-factory nil)
157+
true (catch Throwable ex (log/info ex) false)))
158+
140159
(defmethod driver/execute-reducible-query :neo4j
141160
[driver query chans respond]
142-
(neo4j.execute/execute-reducible-query driver query chans respond))
161+
(if (cypher? query) (neo4j.execute/execute-reducible-query->cypher driver query chans respond) (neo4j.execute/execute-reducible-query driver query chans respond)))

src/metabase/driver/neo4j/execute.clj

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
(ns metabase.driver.neo4j.execute
2+
(:import [org.neo4j.driver
3+
Driver])
24
(:require
5+
[metabase.driver.neo4j.util
6+
:refer [with-neo-connection]]
7+
[neo4clj.client :as neo4j]
8+
[clojure.core.async :as a]
9+
[clojure.tools.logging :as log]
310
[metabase.driver.sql-jdbc
411
[execute :as sql-jdbc.execute]]
512
[metabase.mbql.util :as mbql.u]
613
[metabase.query-processor
714
[context :as context]
815
[interface :as qp.i]
916
[store :as qp.store]
17+
[reducible :as qp.reducible]
1018
[timezone :as qp.timezone]]))
1119

1220

1321
; We want to do this in order to avoid remarks and commenting which affect the simba JDBC driver
22+
23+
1424
(defn execute-reducible-query
1525
"Implementation of `execute-reducible-query` for neo4j bi connector driver
1626
Copied as is from sql-jdbc/execute"
@@ -25,4 +35,34 @@
2535
rs (sql-jdbc.execute/execute-query! driver stmt)]
2636
(let [rsmeta (.getMetaData rs)
2737
results-metadata {:cols (sql-jdbc.execute/column-metadata driver rsmeta)}]
28-
(respond results-metadata (sql-jdbc.execute/reducible-rows driver rs rsmeta (context/canceled-chan context)))))))
38+
(respond results-metadata (sql-jdbc.execute/reducible-rows driver rs rsmeta (context/canceled-chan context)))))))
39+
40+
; TODO add with-neo4j-connection macro
41+
(defn get-neo-connection
42+
[{host :host port :port user :user password :password dbname :dbname}]
43+
(let [base (str "bolt://" host ":" port)
44+
url (if dbname (str base "/" dbname) base)]
45+
(if password (neo4j/connect url user password) (neo4j/connect url user))))
46+
47+
(defn get-cypher-columns [result]
48+
(if (seq? result)
49+
{:cols (into [] (map #(assoc {} :name %) (keys (first (take 1 result)))))}
50+
{:cols [{:name "result"}]}))
51+
52+
(defn execute-reducible-query->cypher
53+
"Process and run a native cypher query."
54+
[_ {{query :query} :native} context respond]
55+
(log/info "Executing reducible query for cypher")
56+
(with-neo-connection [^Driver connection (:details (qp.store/database))]
57+
(let [results (volatile! (neo4j/execute! connection query))
58+
nonseq-val (volatile! false)
59+
columns (get-cypher-columns @results)
60+
row-thunk #(if-not (seq? @results) ((if-not @nonseq-val (vreset! nonseq-val true) @results) nil)
61+
(let [old @results]
62+
(vswap! results (fn [state] (drop 1 state)))
63+
(vals (first (take 1 old)))))]
64+
; handle cancellation
65+
(a/go
66+
(when (a/<! (context/canceled-chan context))
67+
(neo4j/disconnect connection)))
68+
(respond columns (qp.reducible/reducible-rows row-thunk (context/canceled-chan context))))))

src/metabase/driver/neo4j/util.clj

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
(ns metabase.driver.neo4j.util
2+
(:import [org.neo4j.driver
3+
Driver])
4+
(:require
5+
[neo4clj.client :as neo4j]
6+
[clojure.tools.logging :as log]
7+
[metabase.util
8+
[i18n :refer [trs]]]
9+
[metabase
10+
[util :as u]]))
11+
12+
(def ^:dynamic ^Driver *neo-connection*
13+
"Connection to a Neo4j database. Bound by top-level `with-neo-connection` so it may be reused within its body."
14+
nil)
15+
16+
(defn get-neo-connection
17+
[{host :host port :port user :user password :password dbname :dbname}]
18+
(let [base (str "bolt://" host ":" port)
19+
url (if dbname (str base "/" dbname) base)]
20+
(if password (neo4j/connect url user password) (neo4j/connect url user))))
21+
22+
(defn -with-neo-connection
23+
"Run `f` with a new connection (bound to `*neo-connection*`) with `details`. Don't use this directly; use
24+
`with-neo-connection`."
25+
[f details]
26+
(let [connection (get-neo-connection details)]
27+
(log/debug (u/format-color 'cyan (trs "Opened new Neo4j connection.")))
28+
(try
29+
(binding [*neo-connection* connection]
30+
(f *neo-connection*))
31+
(finally
32+
(neo4j/disconnect connection)
33+
(log/debug (u/format-color 'cyan (trs "Closed Neo4j connection.")))))))
34+
35+
; TODO: use steady connection pool with idle timeout instead of one-off use
36+
(defmacro with-neo-connection
37+
"Open a new neo4j connection based on DB ``details`, bind connection to `binding`, execute `body`, and
38+
close the connection. The DB connection is re-used by subsequent calls to `with-neo-connection` within
39+
`body`."
40+
[[binding details] & body]
41+
`(let [f# (fn [~binding]
42+
~@body)]
43+
(if *neo-connection*
44+
(f# *neo-connection*)
45+
(-with-neo-connection f# ~details))))

0 commit comments

Comments
 (0)