Skip to content

Commit 3189e84

Browse files
committed
wip rewrite
1 parent 468422d commit 3189e84

20 files changed

+91
-203
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,20 @@
66

77
A Graph Database for Julia, built on top of [SQLite.jl](https://github.com/JuliaDatabases/SQLite.jl).
88

9+
<br><br>
10+
911
## Quickstart
1012

13+
### Definitions
14+
15+
SQLiteGraph.jl uses the [Property Graph Model of the Cypher Query Language (PDF)](https://s3.amazonaws.com/artifacts.opencypher.org/openCypher9.pdf).
16+
17+
- A **Node** describes a discrete object in a domain.
18+
- Nodes can have 0+ **labels** that classify what kind of node they are.
19+
- An **Edge** describes a directional relationship between nodes.
20+
- An edge must have a **type** that classifies the relationship.
21+
- Both edges and nodes can have additional key-value **properties** that provide further information.
22+
1123
```julia
1224
using SQLiteGraph
1325

sql/delete-edge.sql

-1
This file was deleted.

sql/delete-node.sql

-1
This file was deleted.

sql/insert-edge.sql

-1
This file was deleted.

sql/insert-node.sql

-1
This file was deleted.

sql/schema.sql

-18
This file was deleted.

sql/search-edges-inbound.sql

-1
This file was deleted.

sql/search-edges-outbound.sql

-1
This file was deleted.

sql/search-edges.sql

-3
This file was deleted.

sql/search-node-by-id.sql

-1
This file was deleted.

sql/search-node.sql

-1
This file was deleted.

sql/traverse-inbound.sql

-5
This file was deleted.

sql/traverse-outbound.sql

-5
This file was deleted.

sql/traverse-with-bodies-inbound.sql

-7
This file was deleted.

sql/traverse-with-bodies-outbound.sql

-7
This file was deleted.

sql/traverse-with-bodies.sql

-9
This file was deleted.

sql/traverse.sql

-7
This file was deleted.

sql/update-node.sql

-1
This file was deleted.

src/SQLiteGraph.jl

+60-92
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export DB, Node, Edge
1010
#-----------------------------------------------------------------------------# utils
1111
function single_result_execute(db, stmt, args...)
1212
ex = execute(db, stmt, args...)
13-
isempty(ex) ? nothing : values(first(ex))[1]
13+
isempty(ex) ? nothing : first(first(ex))
1414
end
1515

1616
function print_props(io::IO, o::Config)
@@ -22,19 +22,6 @@ end
2222

2323

2424
#-----------------------------------------------------------------------------# Model
25-
# Nodes describe entities (discrete objects) of a domain.
26-
# Nodes can have zero or more labels to define (classify) what kind of nodes they are.
27-
# Nodes and relationships can have properties (key-value pairs), which further describe them.
28-
29-
# Relationships describes a connection between a source node and a target node.
30-
# Relationships always has a direction (one direction).
31-
# Relationships must have a type (one type) to define (classify) what type of relationship they are.
32-
33-
# Nouns-nodes, Adjectives-properties, Verbs-relationship, Adverbs-properties on relationship
34-
35-
# Property Graph Model on page 4
36-
# https://s3.amazonaws.com/artifacts.opencypher.org/openCypher9.pdf
37-
3825
struct Node
3926
id::Int
4027
labels::Vector{String}
@@ -55,6 +42,7 @@ function Base.show(io::IO, o::Node)
5542
end
5643
end
5744
args(n::Node) = (n.id, join(n.labels, ';'), JSON3.write(n.props))
45+
Base.:(==)(a::Node, b::Node) = all(getfield(a,f) == getfield(b,f) for f in fieldnames(Node))
5846

5947

6048
struct Edge
@@ -77,7 +65,9 @@ function Base.show(io::IO, o::Edge)
7765
printstyled(io, join(keys(o.props), ", "), color=:light_green)
7866
end
7967
end
80-
args(e::Edge) = (e.source, e.target, join(e.type, ';'), JSON3.write(e.props))
68+
args(e::Edge) = (e.source, e.target, e.type, JSON3.write(e.props))
69+
Base.:(==)(a::Edge, b::Edge) = all(getfield(a,f) == getfield(b,f) for f in fieldnames(Edge))
70+
8171

8272
#-----------------------------------------------------------------------------# DB
8373
struct DB
@@ -103,11 +93,13 @@ struct DB
10393
type TEXT NOT NULL,
10494
props TEXT,
10595
FOREIGN KEY(source) REFERENCES nodes(id),
106-
FOREIGN KEY(target) REFERENCES nodes(id)
96+
FOREIGN KEY(target) REFERENCES nodes(id),
97+
UNIQUE(source, target, type)
10798
);",
10899
"CREATE INDEX IF NOT EXISTS source_idx ON edges(source);",
109100
"CREATE INDEX IF NOT EXISTS target_idx ON edges(target);",
110101
"CREATE INDEX IF NOT EXISTS type_idx ON edges(type);",
102+
"CREATE INDEX IF NOT EXISTS source_target_type_idx ON edges(source, target, type);"
111103
])
112104
new(db)
113105
end
@@ -125,92 +117,68 @@ Base.length(db::DB) = n_nodes(db)
125117
Base.size(db::DB) = (n_nodes=n_nodes(db), n_edges=n_edges(db))
126118

127119
#-----------------------------------------------------------------------------# nodes
120+
function Base.push!(db::DB, node::Node; upsert=false)
121+
upsert ?
122+
execute(db, "INSERT INTO nodes VALUES(?, ?, json(?)) ON CONFLICT(id) DO UPDATE SET labels=excluded.labels, props=excluded.props", args(node)) :
123+
execute(db, "INSERT INTO nodes VALUES(?, ?, json(?))", args(node))
124+
db
125+
end
126+
128127
function Base.getindex(db::DB, id::Integer)
129128
res = execute(db, "SELECT * FROM nodes WHERE id = ?", (id,))
130-
isempty(res) ? throw(BoundsError(db, id)) : Node(first(res))
129+
isempty(res) ? error("Node $id does not exist.") : Node(first(res))
131130
end
131+
function Base.getindex(db::DB, ::Colon)
132+
res = execute(db, "SELECT * FROM nodes")
133+
isempty(res) ? error("No nodes exist yet.") : (Node(row) for row in res)
134+
end
135+
132136

133-
function Base.push!(db::DB, node::Node)
134-
res = execute(db, "SELECT * FROM nodes WHERE id=?", (node.id,))
135-
isempty(res) ?
136-
execute(db, "INSERT INTO nodes VALUES(?, ?, json(?))", args(node)) :
137-
error("Node with id=$(node.id) already exists in graph. Use `insert!` to overwrite.")
137+
#-----------------------------------------------------------------------------# edges
138+
function Base.push!(db::DB, edge::Edge; upsert=false)
139+
i, j = edge.source, edge.target
140+
check = single_result_execute(db, "SELECT COUNT(*) FROM nodes WHERE id=? OR id=?", (i, j))
141+
(isnothing(check) || check < 2) && error("Nodes $i and $j must exist in order for an edge to connect them.")
142+
upsert ?
143+
execute(db, "INSERT INTO edges VALUES(?, ?, ?, json(?)) ON CONFLICT(source,target,type) DO UPDATE SET props=excluded.props", args(edge)) :
144+
execute(db, "INSERT INTO edges VALUES(?, ?, ?, json(?))", args(edge))
138145
db
139146
end
140-
function Base.insert!(db::DB, node::Node)
141-
execute(db, "INSERT INTO nodes VALUES(?, ?, json(?)) ON CONFLICT(id) DO UPDATE SET labels=excluded.labels, props=excluded.props", args(node))
142-
db
147+
148+
function Base.getindex(db::DB, i::Integer, j::Integer, type::AbstractString)
149+
res = execute(db, "SELECT * FROM edges WHERE source=? AND target=? AND type=?", (i,j,type))
150+
isempty(res) ? error("Edge $i$type$j does not exist.") : Edge(first(res))
151+
end
152+
function Base.getindex(db::DB, i::Integer, j::Integer, ::Colon = Colon())
153+
res = execute(db, "SELECT * FROM edges WHERE source=? AND target=?", (i, j))
154+
isempty(res) ? error("No edges connect nodes $i$j.") : (Edge(row) for row in res)
143155
end
144156

157+
function Base.getindex(db::DB, ::Colon, j::Integer, type::AbstractString)
158+
res = execute(db, "SELECT * FROM edges WHERE target=? AND type=?", (j, type))
159+
isempty(res) ? error("No incoming edges $type$j") : (Edge(row) for row in res)
160+
end
161+
function Base.getindex(db::DB, i::Colon, j::Integer, ::Colon=Colon())
162+
res = execute(db, "SELECT * FROM edges WHERE target=?", (j,))
163+
isempty(res) ? error("No incoming edges into node $j") : (Edge(row) for row in res)
164+
end
145165

166+
function Base.getindex(db::DB, i::Integer, ::Colon, type::AbstractString)
167+
res = execute(db, "SELECT * FROM edges WHERE source=? AND type=?", (i,type))
168+
isempty(res) ? error("No outgoing edges $type$i") : (Edge(row) for row in res)
169+
end
170+
function Base.getindex(db::DB, i::Integer, ::Colon, ::Colon=Colon())
171+
res = execute(db, "SELECT * FROM edges WHERE source=?", (i,))
172+
isempty(res) ? error("No outgoing edges from node $i") : (Edge(row) for row in res)
173+
end
146174

147175

148-
# #-----------------------------------------------------------------------------# get/set nodes
149-
# function Base.setindex!(db::DB, props, id::Integer)
150-
# id ≤ length(db) + 1 || error("Cannot add node ID=$id to DB with $(length(db)) nodes. IDs must be added sequentially.")
151-
# execute(db, "INSERT INTO nodes VALUES(?, json(?))", (id, JSON3.write(props)))
152-
# db
153-
# end
154-
# function Base.getindex(db::DB, id::Integer)
155-
# res = single_result_execute(db, "SELECT props FROM nodes WHERE id = ?", (id,))
156-
# isnothing(res) ? throw(BoundsError(db, id)) : Node(id, res)
157-
# end
158-
# Base.getindex(db::DB, ids::AbstractArray{<:Integer}) = (getindex(db, id) for id in ids)
159-
# function Base.getindex(db::DB, ::Colon)
160-
# res = execute(db, "SELECT props from nodes")
161-
# (Node(i, row.props) for (i,row) in enumerate(res))
162-
# end
163-
# function Base.deleteat!(db::DB, id::Integer)
164-
# execute(db, "DELETE FROM nodes WHERE id = ?", (id,))
165-
# execute(db, "DELETE FROM edges WHERE source = ? OR target = ?", (id, id))
166-
# db
167-
# end
168-
169-
# #-----------------------------------------------------------------------------# get/set edges
170-
# function Base.setindex!(db::DB, props, i::Integer, j::Integer)
171-
# execute(db, "INSERT INTO edges VALUES(?, ?, json(?))", (i, j, JSON3.write(props)))
172-
# db
173-
# end
174-
# function Base.getindex(db::DB, i::Integer, j::Integer)
175-
# res = single_result_execute(db, "SELECT props FROM edges WHERE source = ? AND target = ? ", (i,j))
176-
# isnothing(res) ? res : Edge(i, j, res)
177-
# end
178-
# Base.getindex(db::DB, i::Integer, js::AbstractArray{<:Integer}) = filter!(!isnothing, getindex.(db, i, js))
179-
# Base.getindex(db::DB, is::AbstractArray{<:Integer}, j::Integer) = filter!(!isnothing, getindex.(db, is, j))
180-
# function Base.getindex(db::DB, is::AbstractArray{<:Integer}, js::AbstractArray{<:Integer})
181-
# res = vcat((getindex(db, i, js) for i in is)...)
182-
# isempty(res) ? nothing : res
183-
# end
184-
# function Base.getindex(db::DB, i::Integer, ::Colon)
185-
# res = execute(db, "SELECT * FROM edges WHERE source=?", (i,))
186-
# isempty(res) ? nothing : (Edge(row...) for row in res)
187-
# end
188-
# function Base.getindex(db::DB, ::Colon, j::Integer)
189-
# res = execute(db, "SELECT * FROM edges WHERE target=?", (j,))
190-
# isempty(res) ? nothing : (Edge(row...) for row in res)
191-
# end
192-
# function Base.getindex(db::DB, ::Colon, ::Colon)
193-
# res = execute(db, "SELECT * from edges")
194-
# isempty(res) ? nothing : (Edge(row...) for row in res)
195-
# end
196-
# Base.getindex(db::DB, is::AbstractArray{<:Integer}, ::Colon) = filter!(!isnothing, getindex.(db, is, :))
197-
# Base.getindex(db::DB, ::Colon, js::AbstractArray{<:Integer}) = filter!(!isnothing, getindex.(db, :, js))
198-
199-
# function Base.deleteat!(db::DB, i::Integer, j::Integer)
200-
# execute(db, "DELETE FROM edges WHERE source = ? AND target = ?", (i, j))
201-
# db
202-
# end
203-
204-
# #-----------------------------------------------------------------------------# interfaces
205-
# Base.length(db::DB) = n_nodes(db)
206-
# Base.size(db::DB) = (nodes=n_nodes(db), edges=n_edges(db))
207-
# Base.lastindex(db::DB) = length(db)
208-
# Base.axes(db::DB, i) = size(db)[i]
209-
210-
# Broadcast.broadcastable(db::DB) = Ref(db)
211-
212-
# #-----------------------------------------------------------------------------# iterators
213-
# eachnode(db::DB) = (db[i] for i in 1:length(db))
214-
# eachedge(db::DB) = (Edge(row...) for row in execute(db, "SELECT * from edges"))
176+
#-----------------------------------------------------------------------------# interfaces
177+
Base.length(db::DB) = n_nodes(db)
178+
Base.size(db::DB) = (nodes=n_nodes(db), edges=n_edges(db))
179+
Base.lastindex(db::DB) = length(db)
180+
Base.axes(db::DB, i) = size(db)[i]
181+
182+
Broadcast.broadcastable(db::DB) = Ref(db)
215183

216184
end

test/runtests.jl

+19-41
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,29 @@ using JSON3
44

55
@testset "SQLiteGraph.jl" begin
66
db = DB()
7-
node1 = (x=1, y=2)
8-
node2 = (x=1, y=3)
9-
edge1 = (a=1, b=2)
10-
edge2 = (a=3, b=4)
117

12-
@testset "empty DB" begin
13-
@test length(db) == 0
14-
@test size(db) == (nodes=0, edges=0)
15-
@test_throws BoundsError db[5]
16-
@test isnothing(db[1,2])
17-
end
18-
@testset "setindex! & getindex" begin
19-
db[1] = node1
20-
db[2] = node2
21-
db[1,2] = edge1
22-
db[2,1] = edge2
23-
@test db[1] isa Node{String}
24-
@test db[2] isa Node{String}
25-
@test db[1,2] isa Edge{String}
26-
@testset "getindex Range" begin
27-
@test collect(db[1:2]) isa Vector{Node{String}}
28-
q = collect(db[1:2])
29-
@test q[1] == Node(1, JSON3.write(node1))
30-
@test q[2] == Node(2, JSON3.write(node2))
31-
@test first(db[1:2,2]) == Edge(1,2,JSON3.write(edge1))
32-
end
33-
end
34-
@testset "find_nodes / find_edges" begin
35-
res = collect(find_nodes(db, y=2))
36-
@test length(res) == 1
37-
@test res[1] == Node(1, JSON3.write(node1))
8+
n1 = Node(1, "label 1", "label 2"; prop1=1, prop2=2)
9+
n1_2 = Node(1, "label 3", prop3=3)
10+
n2 = Node(2)
3811

39-
res = collect(find_nodes(db, r"y"))
40-
@test length(res) == 2
41-
@test res[1] == Node(1, JSON3.write(node1))
42-
@test res[2] == Node(2, JSON3.write(node2))
12+
@testset "Adding Nodes" begin
4313

44-
res = collect(find_edges(db, a=1))
45-
@test length(res) == 1
46-
@test res[1] == Edge(1,2, JSON3.write(edge1))
14+
push!(db, n1)
15+
@test db[1] == n1
16+
@test_throws Exception push!(db, n1)
4717

48-
res = collect(find_edges(db, r"a"))
49-
@test length(res) == 2
50-
@test res[1] == Edge(1,2, JSON3.write(edge1))
51-
@test res[2] == Edge(2,1, JSON3.write(edge2))
5218

19+
push!(db, n1_2; upsert=true)
20+
@test db[1] == n1_2
21+
end
22+
@testset "Adding Edges" begin
23+
push!(db, Node(2))
24+
e1 = Edge(1, 2, "type")
25+
push!(db, e1)
26+
@test e1 == db[1,2,"type"]
27+
end
28+
@testset "Querying Nodes" begin
29+
@test length(collect(db[:])) == 2
30+
@test db[1] == n1_2
5331
end
5432
end

0 commit comments

Comments
 (0)