Skip to content

Commit 248cdec

Browse files
committed
rewrite readme, get tests online for rewrite
1 parent a07227a commit 248cdec

File tree

3 files changed

+152
-187
lines changed

3 files changed

+152
-187
lines changed

README.md

Lines changed: 36 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -8,132 +8,74 @@ A Graph Database for Julia, built on top of [SQLite.jl](https://github.com/Julia
88

99
<br><br>
1010

11-
## Quickstart
1211

13-
### Definitions
12+
## Definitions
1413

1514
SQLiteGraph.jl uses the [Property Graph Model of the Cypher Query Language (PDF)](https://s3.amazonaws.com/artifacts.opencypher.org/openCypher9.pdf).
1615

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.
16+
- A **_Node_** describes a discrete object in a domain.
17+
- Nodes can have 0+ **_labels_** that classify what kind of node they are.
18+
- An **_Edge_** describes a directional relationship between nodes.
19+
- An edge must have a **_type_** that classifies the relationship.
20+
- Both edges and nodes can have additional key-value **_properties_** that provide further information.
2221

23-
```julia
24-
using SQLiteGraph
25-
26-
db = DB()
27-
# SQLiteGraph.DB(":memory:") (0 nodes, 0 edges)
28-
29-
db[1] = Config(type="person", name="Fred")
30-
31-
db[2] = Config(type="person", name="Robert Ford")
32-
33-
db[2, 1] = Config(shot=true)
34-
```
35-
36-
37-
38-
- Nodes and edges must have "properties" (something that is `JSON3.write`-able), even if its `nothing`.
39-
- Nodes must have an id (`Int`) in ascending order starting with `1`.
40-
- Add nodes and edges with `setindex!`
41-
- E.g. Node 1: `db[1] = (x=1, y=2)`
42-
- E.g. Edge 1 → 2: `db[1, 2] = (a=3, b=4)`
43-
- Query nodes and edges with `getindex` (as well as `find_nodes`/`find_edges`).
44-
- E.g. Nodes 1, 2, and 3: `db[1:3]`
45-
- E.g. Edges from 1 to 2 or 3: `db[1, 2:3]`
46-
- Returned objects are `Node` or `Edge` (or generator if multiple objects queried):
47-
- `Node` and `Edge` are simple structs.
48-
49-
```julia
50-
struct Node
51-
id::Int
52-
props::Config
53-
end
22+
<br><br>
5423

55-
struct Edge
56-
source::Int
57-
target::Int
58-
props::config
59-
end
60-
```
24+
## Edges and Nodes
6125

62-
### Creating a Graph Database
26+
- Nodes and Edges have a simple representation:
6327

6428
```julia
65-
using SQLiteGraph
29+
struct Node
30+
id::Int
31+
labels::Vector{String}
32+
props::EasyConfig.Config
33+
end
6634

67-
db = DB()
68-
# SQLiteGraph.DB(":memory:") (0 nodes, 0 edges)
35+
struct Edge
36+
source::Int
37+
target::Int
38+
type::String
39+
props::EasyConfig.Config
40+
end
6941
```
7042

71-
### Adding Nodes
43+
- With simple constructors:
7244

7345
```julia
74-
# properties must be `JSON3.write`-able (saved in the SQLite database as TEXT)
75-
db[1] = (x=1, y=2)
46+
Node(id, labels...; props...)
7647

77-
db[2] = (x=1, y=10)
78-
79-
db[1]
80-
# Node 1
81-
# • y: 2
82-
# • x: 1
48+
Edge(source_id, target_id, type; props...)
8349
```
8450

85-
### Adding Edges
86-
87-
```julia
88-
db[1, 2] = (a=1, b=2)
89-
90-
db[1, 2]
91-
# Edge 1 → 2
92-
# • a: 1
93-
# • b: 2
94-
```
51+
<br><br>
9552

96-
## Iteration
53+
## Adding Elements to the Graph
9754

9855
```julia
99-
for node in eachnode(db)
100-
println(getfield(node, :id))
101-
end
102-
```
56+
using SQLiteGraph
10357

104-
### Querying Edges Based on Node ID
58+
db = DB()
10559

106-
```julia
107-
db[1, :] # all outgoing edges from node 1
60+
insert!(db, Node(1, "Person", "Actor"; name="Tom Hanks"))
10861

109-
db[1, 2:5] # outgoing edges from node 1 to any of nodes 2,3,4,5
62+
insert!(db, Node(2, "Movie"; title="Forest Gump"))
11063

111-
db[:, 2] # all incoming edges to node 2
64+
insert!(db, Edge(1, 2, "Acts In"))
11265
```
11366

114-
### Querying Based on Properties
115-
116-
- multiple keyword args are a logical "AND"
67+
<br><br>
11768

118-
```julia
119-
find_nodes(db, x=1)
69+
## Editing Elements
12070

121-
find_edges(db, b=2)
122-
```
123-
124-
- You can also query based on Regex matches of the `TEXT` properties:
71+
`insert!` will not replace an existing node or edge. Instead, use `replace!`.
12572

12673
```julia
127-
find_nodes(db, r"x")
128-
129-
find_edges(db, r"\"b\":2")
74+
replace!(db, Node(2, "Movie"; title="Forest Gump", genre="Drama"))
13075
```
13176

77+
<br><br>
78+
13279
## ✨ Attribution ✨
13380

13481
SQLiteGraph is **STRONGLY** influenced (much has been copied verbatim) from [https://github.com/dpapathanasiou/simple-graph](https://github.com/dpapathanasiou/simple-graph).
135-
136-
## TODOs
137-
138-
- Prepare SQL into compiled `SQLite.Stmt`s.
139-
- traversal algorithms.

src/SQLiteGraph.jl

Lines changed: 63 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,14 @@ struct Node
2828
props::Config
2929
end
3030
Node(id::Int, labels::String...; props...) = Node(id, collect(labels), Config(props))
31-
Node(row::SQLite.Row) = Node(row.id, split(row.labels, ';'), JSON3.read(row.props, Config))
31+
Node(row::SQLite.Row) = Node(row.id, split(row.labels, ';', keepempty=false), JSON3.read(row.props, Config))
3232
function Base.show(io::IO, o::Node)
33-
printstyled(io, "Node(", color=:light_cyan)
34-
printstyled(io, join(repr.(o.labels), ", "), color=:light_yellow)
35-
printstyled(io, ", ", o.id, color=:light_black)
36-
printstyled(io, ")", color=:light_cyan)
37-
if isempty(o.props)
38-
printstyled(io, " with no props", color=:light_black)
39-
else
40-
printstyled(io, " with props: ", color=:light_black)
41-
printstyled(io, join(keys(o.props), ", "), color=:light_green)
42-
end
33+
print(io, "Node($(o.id)")
34+
!isempty(o.labels) && print(io, ", ", join(repr.(o.labels), ", "))
35+
!isempty(o.props) && print(io, "; ", ("$k=$v" for (k,v) in pairs(o.props))...)
36+
print(io, ')')
4337
end
44-
args(n::Node) = (n.id, join(n.labels, ';'), JSON3.write(n.props))
38+
args(n::Node) = (n.id, isempty(n.labels) ? "" : join(n.labels, ';'), JSON3.write(n.props))
4539
Base.:(==)(a::Node, b::Node) = all(getfield(a,f) == getfield(b,f) for f in fieldnames(Node))
4640

4741

@@ -54,16 +48,9 @@ end
5448
Edge(src::Int, tgt::Int, type::String; props...) = Edge(src, tgt, type, Config(props))
5549
Edge(row::SQLite.Row) = Edge(row.source, row.target, row.type, JSON3.read(row.props, Config))
5650
function Base.show(io::IO, o::Edge)
57-
printstyled(io, "Edge(", color=:light_cyan)
58-
printstyled(io, repr(o.type), color=:light_yellow)
59-
printstyled(io, ", $(o.source)$(o.target)", color=:light_black)
60-
printstyled(io, ")", color=:light_cyan)
61-
if isempty(o.props)
62-
printstyled(io, " with no props", color=:light_black)
63-
else
64-
printstyled(io, " with props: ", color=:light_black)
65-
printstyled(io, join(keys(o.props), ", "), color=:light_green)
66-
end
51+
print(io, "Edge($(o.source), $(o.target), ", repr(o.type))
52+
!isempty(o.props) && print(io, "; ", ("$k=$v" for (k,v) in pairs(o.props))...)
53+
print(io, ')')
6754
end
6855
args(e::Edge) = (e.source, e.target, e.type, JSON3.write(e.props))
6956
Base.:(==)(a::Edge, b::Edge) = all(getfield(a,f) == getfield(b,f) for f in fieldnames(Edge))
@@ -76,30 +63,25 @@ struct DB
7663
function DB(file::String = ":memory:")
7764
db = SQLite.DB(file)
7865
foreach(x -> execute(db, x), [
66+
"PRAGMA foreign_keys = ON;",
7967
# nodes
8068
"CREATE TABLE IF NOT EXISTS nodes (
8169
id INTEGER NOT NULL UNIQUE PRIMARY KEY,
82-
labels TEXT,
83-
props TEXT,
84-
UNIQUE(id) ON CONFLICT REPLACE
70+
labels TEXT NOT NULL,
71+
props TEXT NOT NULL
8572
);",
86-
"CREATE INDEX IF NOT EXISTS id_idx ON nodes(id);",
87-
"CREATE INDEX IF NOT EXISTS labels_idx ON nodes(labels);",
8873

8974
# edges
9075
"CREATE TABLE IF NOT EXISTS edges (
91-
source INTEGER NOT NULL,
92-
target INTEGER NOT NULL,
76+
source INTEGER NOT NULL REFERENCES nodes(id),
77+
target INTEGER NOT NULL REFERENCES nodes(id),
9378
type TEXT NOT NULL,
94-
props TEXT,
95-
FOREIGN KEY(source) REFERENCES nodes(id),
96-
FOREIGN KEY(target) REFERENCES nodes(id),
97-
UNIQUE(source, target, type)
79+
props TEXT NOT NULL,
80+
PRIMARY KEY (source, target, type)
9881
);",
9982
"CREATE INDEX IF NOT EXISTS source_idx ON edges(source);",
10083
"CREATE INDEX IF NOT EXISTS target_idx ON edges(target);",
10184
"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);"
10385
])
10486
new(db)
10587
end
@@ -114,16 +96,33 @@ n_nodes(db::DB) = single_result_execute(db, "SELECT Count(*) FROM nodes")
11496
n_edges(db::DB) = single_result_execute(db, "SELECT Count(*) FROM edges")
11597

11698
Base.length(db::DB) = n_nodes(db)
117-
Base.size(db::DB) = (n_nodes=n_nodes(db), n_edges=n_edges(db))
99+
Base.size(db::DB) = (nodes=n_nodes(db), edges=n_edges(db))
100+
Base.lastindex(db::DB) = length(db)
101+
Base.axes(db::DB, i) = size(db)[i]
102+
103+
Broadcast.broadcastable(db::DB) = Ref(db)
104+
105+
#-----------------------------------------------------------------------------# insert!
106+
function Base.insert!(db::DB, node::Node)
107+
execute(db, "INSERT INTO nodes VALUES(?, ?, json(?))", args(node))
108+
db
109+
end
110+
function Base.insert!(db::DB, edge::Edge)
111+
execute(db, "INSERT INTO edges VALUES(?, ?, ?, json(?))", args(edge))
112+
db
113+
end
118114

119-
#-----------------------------------------------------------------------------# 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))
115+
#-----------------------------------------------------------------------------# replace!
116+
function Base.replace!(db::DB, node::Node)
117+
execute(db, "INSERT INTO nodes VALUES(?, ?, json(?)) ON CONFLICT(id) DO UPDATE SET labels=excluded.labels, props=excluded.props", args(node))
118+
db
119+
end
120+
function Base.replace!(db::DB, edge::Edge)
121+
execute(db, "INSERT INTO edges VALUES(?, ?, ?, json(?)) ON CONFLICT(source,target,type) DO UPDATE SET props=excluded.props", args(edge))
124122
db
125123
end
126124

125+
#-----------------------------------------------------------------------------# getindex (Node)
127126
function Base.getindex(db::DB, id::Integer)
128127
res = execute(db, "SELECT * FROM nodes WHERE id = ?", (id,))
129128
isempty(res) ? error("Node $id does not exist.") : Node(first(res))
@@ -133,52 +132,45 @@ function Base.getindex(db::DB, ::Colon)
133132
isempty(res) ? error("No nodes exist yet.") : (Node(row) for row in res)
134133
end
135134

136-
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))
145-
db
146-
end
147-
135+
#-----------------------------------------------------------------------------# getindex (Edge)
136+
# all specified
148137
function Base.getindex(db::DB, i::Integer, j::Integer, type::AbstractString)
149138
res = execute(db, "SELECT * FROM edges WHERE source=? AND target=? AND type=?", (i,j,type))
150139
isempty(res) ? error("Edge $i$type$j does not exist.") : Edge(first(res))
151140
end
152-
function Base.getindex(db::DB, i::Integer, j::Integer, ::Colon = Colon())
141+
142+
# one colon
143+
function Base.getindex(db::DB, i::Integer, j::Integer, ::Colon)
153144
res = execute(db, "SELECT * FROM edges WHERE source=? AND target=?", (i, j))
154145
isempty(res) ? error("No edges connect nodes $i$j.") : (Edge(row) for row in res)
155146
end
156-
147+
function Base.getindex(db::DB, i::Integer, ::Colon, type::AbstractString)
148+
res = execute(db, "SELECT * FROM edges WHERE source=? AND type=?", (i,type))
149+
isempty(res) ? error("No outgoing edges $type$i") : (Edge(row) for row in res)
150+
end
157151
function Base.getindex(db::DB, ::Colon, j::Integer, type::AbstractString)
158152
res = execute(db, "SELECT * FROM edges WHERE target=? AND type=?", (j, type))
159153
isempty(res) ? error("No incoming edges $type$j") : (Edge(row) for row in res)
160154
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
165155

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())
156+
# two colons
157+
function Base.getindex(db::DB, i::Integer, ::Colon, ::Colon)
171158
res = execute(db, "SELECT * FROM edges WHERE source=?", (i,))
172159
isempty(res) ? error("No outgoing edges from node $i") : (Edge(row) for row in res)
173160
end
161+
function Base.getindex(db::DB, i::Colon, j::Integer, ::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
165+
function Base.getindex(db::DB, ::Colon, ::Colon, type::AbstractString)
166+
res = execute(db, "SELECT * FROM edges WHERE type=?", (type,))
167+
isempty(res) ? error("No edges with type $type") : (Edge(row) for row in res)
168+
end
174169

175-
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)
170+
# all colons
171+
function Base.getindex(db::DB, ::Colon, ::Colon, ::Colon)
172+
res = execute(db, "SELECT * FROM edges")
173+
isempty(res) ? error("No edges exist yet.") : (Edge(row) for row in res)
174+
end
183175

184176
end

0 commit comments

Comments
 (0)