pglpg 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +101 -0
- data/README.md +227 -8
- data/lib/pglpg/context.rb +55 -8
- data/lib/pglpg/element.rb +8 -1
- data/lib/pglpg/errors.rb +19 -0
- data/lib/pglpg/graph.rb +5 -2
- data/lib/pglpg/match_parser.rb +308 -0
- data/lib/pglpg/model/base.rb +11 -1
- data/lib/pglpg/model/edge.rb +67 -2
- data/lib/pglpg/model/node.rb +13 -8
- data/lib/pglpg/model/relation.rb +9 -2
- data/lib/pglpg/query.rb +21 -7
- data/lib/pglpg/reachability.rb +2 -0
- data/lib/pglpg/schema/edge_type.rb +28 -6
- data/lib/pglpg/schema/element_type.rb +80 -7
- data/lib/pglpg/schema/graph_type.rb +21 -7
- data/lib/pglpg/schema/key.rb +82 -0
- data/lib/pglpg/schema/node_type.rb +7 -31
- data/lib/pglpg/storage.rb +121 -8
- data/lib/pglpg/temporal.rb +48 -0
- data/lib/pglpg/testing/leak_examples.rb +43 -15
- data/lib/pglpg/testing.rb +10 -0
- data/lib/pglpg/version.rb +1 -1
- data/lib/pglpg/writer.rb +89 -40
- data/lib/pglpg.rb +3 -0
- metadata +4 -1
|
@@ -22,33 +22,36 @@
|
|
|
22
22
|
# (rows, order, counts, exists?, first, pagination, lookups) is unchanged.
|
|
23
23
|
# Execution time and timeouts are outside this guarantee.
|
|
24
24
|
RSpec.shared_examples "a pglpg visibility policy" do
|
|
25
|
-
leak_graph = lambda do |visibility, columns|
|
|
25
|
+
leak_graph = lambda do |visibility, columns, temporal, open|
|
|
26
26
|
Pglpg::Graph.define(:pglpg_leak, nodes_table: "pglpg_leak_nodes", edges_table: "pglpg_leak_edges",
|
|
27
27
|
visibility:, write_policy: Pglpg::Testing::AllowAll,
|
|
28
28
|
trusted_writer_allowed: ->(_context) { true }) do
|
|
29
29
|
columns.each { |name, (type, options)| column(name, type, **(options || {})) }
|
|
30
|
-
node :Person do
|
|
30
|
+
node :Person, open: open do
|
|
31
31
|
property :uid, :string, required: true
|
|
32
32
|
property :name, :string
|
|
33
33
|
property :rank, :integer
|
|
34
34
|
property :namespace, :string, required: true
|
|
35
35
|
key :uid, scope: :namespace
|
|
36
36
|
end
|
|
37
|
-
node :Project do
|
|
37
|
+
node :Project, open: open do
|
|
38
38
|
property :uid, :string, required: true
|
|
39
|
-
property :title, :string
|
|
40
|
-
key :uid
|
|
39
|
+
property :title, :string, required: true
|
|
40
|
+
key :uid, :title
|
|
41
41
|
end
|
|
42
42
|
node :Location do
|
|
43
43
|
property :uid, :string, required: true
|
|
44
44
|
key :uid
|
|
45
45
|
end
|
|
46
|
-
edge :WORKS_ON, from: :Person, to: :Project do
|
|
46
|
+
edge :WORKS_ON, from: :Person, to: :Project, temporal: temporal, open: open do
|
|
47
47
|
property :role, :string
|
|
48
|
+
property :uid, :string, required: true
|
|
49
|
+
property :batch, :integer, required: true
|
|
50
|
+
key :uid, :batch
|
|
48
51
|
end
|
|
49
|
-
edge :BASED_AT, from: :Project, to: :Location
|
|
50
|
-
edge :KNOWS, from: :Person, to: :Person
|
|
51
|
-
edge :FRIEND, from: :Person, to: :Person, symmetric: true
|
|
52
|
+
edge :BASED_AT, from: :Project, to: :Location, temporal: temporal
|
|
53
|
+
edge :KNOWS, from: :Person, to: :Person, temporal: temporal
|
|
54
|
+
edge :FRIEND, from: :Person, to: :Person, symmetric: true, temporal: temporal
|
|
52
55
|
end
|
|
53
56
|
end
|
|
54
57
|
|
|
@@ -59,17 +62,32 @@ RSpec.shared_examples "a pglpg visibility policy" do
|
|
|
59
62
|
end
|
|
60
63
|
end
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
let(:
|
|
65
|
+
# Optional: also exercise temporal validity with the application policy.
|
|
66
|
+
let(:pglpg_temporal) { false }
|
|
67
|
+
# Optional: also exercise extra properties through reads/models.
|
|
68
|
+
let(:pglpg_open_types) { false }
|
|
69
|
+
|
|
70
|
+
let(:pglpg_leak_graph) { leak_graph.call(pglpg_visibility, pglpg_columns, pglpg_temporal, pglpg_open_types) }
|
|
71
|
+
let(:pglpg_seed_graph) { leak_graph.call(Pglpg::Testing::SeeAll, pglpg_columns, pglpg_temporal, pglpg_open_types) }
|
|
64
72
|
|
|
65
73
|
it "keeps every observation unchanged when hidden data is added" do
|
|
66
74
|
pglpg_leak_graph.storage.create!
|
|
67
75
|
seed = pglpg_seed_graph.for(viewer: nil).trusted_writer
|
|
68
76
|
vis = pglpg_visible_attrs
|
|
69
77
|
hid = pglpg_hidden_attrs
|
|
70
|
-
person =
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
person = lambda do |uid, name, rank, attrs|
|
|
79
|
+
extras = pglpg_open_types ? { source_note: uid, evidence: ["one", "two"] } : {}
|
|
80
|
+
seed.insert_node(:Person, properties: { uid:, name:, rank:, namespace: attrs == hid ? "hidden" : "visible", **extras }, attrs:)
|
|
81
|
+
end
|
|
82
|
+
node = lambda do |type, uid, attrs, **props|
|
|
83
|
+
extras = pglpg_open_types && type == :Project ? { source_note: uid } : {}
|
|
84
|
+
seed.insert_node(type, properties: { uid:, **props, **extras }, attrs:)
|
|
85
|
+
end
|
|
86
|
+
edge = lambda do |type, from, to, attrs, **props|
|
|
87
|
+
props = props.merge(uid: "#{from.id}-#{to.id}", batch: 1) if type == :WORKS_ON
|
|
88
|
+
props = props.merge(source_note: "evidence", evidence: [1, 2]) if pglpg_open_types && type == :WORKS_ON
|
|
89
|
+
seed.insert_edge(type, from:, to:, properties: props, attrs:, **(pglpg_temporal ? { valid_from: nil } : {}))
|
|
90
|
+
end
|
|
73
91
|
|
|
74
92
|
# Created first, so it has the smallest id: symmetric edges to it are
|
|
75
93
|
# stored with the hidden node in the source column.
|
|
@@ -92,7 +110,7 @@ RSpec.shared_examples "a pglpg visibility policy" do
|
|
|
92
110
|
]
|
|
93
111
|
visible_refs = [p1, p2, p3, j1, j2, l1, *visible_edges]
|
|
94
112
|
|
|
95
|
-
ctx = pglpg_leak_graph.for(viewer: pglpg_viewer)
|
|
113
|
+
ctx = pglpg_leak_graph.for(viewer: pglpg_viewer, **(pglpg_temporal ? { as_of: Date.new(2020, 6, 1) } : {}))
|
|
96
114
|
before = Pglpg::Testing.observe(ctx, visible_refs)
|
|
97
115
|
# Three visible People plus two visible Projects in this fixture.
|
|
98
116
|
raise "lazy association observations are missing" unless before.keys.count { _1.to_s.start_with?("assoc_") } == 5
|
|
@@ -127,6 +145,16 @@ RSpec.shared_examples "a pglpg visibility policy" do
|
|
|
127
145
|
edge.(:KNOWS, h1, h2, hid)
|
|
128
146
|
]
|
|
129
147
|
|
|
148
|
+
if pglpg_temporal
|
|
149
|
+
# Visible but ineligible edges must be just as unobservable as hidden
|
|
150
|
+
# data, including expired shortcuts and a disjoint-time bridge.
|
|
151
|
+
[[p1, p3], [p3, p2]].each do |from, to|
|
|
152
|
+
hidden_refs << seed.insert_edge(:KNOWS, from:, to:, attrs: vis, valid_from: nil, valid_to: Date.new(2020, 6, 1))
|
|
153
|
+
end
|
|
154
|
+
hidden_refs << seed.insert_edge(:FRIEND, from: p1, to: p3, attrs: vis, valid_from: Date.new(2020, 6, 2))
|
|
155
|
+
hidden_refs << seed.insert_edge(:WORKS_ON, from: p1, to: j2, properties: { uid: "expired", batch: 1 }, attrs: vis, valid_from: nil, valid_to: Date.new(2020, 6, 1))
|
|
156
|
+
end
|
|
157
|
+
|
|
130
158
|
after = Pglpg::Testing.observe(ctx, visible_refs)
|
|
131
159
|
before.each_key { |name| expect(after[name]).to eq(before[name]), "observation #{name} changed" }
|
|
132
160
|
|
data/lib/pglpg/testing.rb
CHANGED
|
@@ -25,6 +25,7 @@ module Pglpg
|
|
|
25
25
|
persons: -> { m.().node(:p, :Person).order(p: :name) },
|
|
26
26
|
works_on: -> { m.().node(:p, :Person).out(:WORKS_ON, :j, :Project, edge: :w) },
|
|
27
27
|
two_hops: -> { m.().node(:p, :Person).out(:WORKS_ON, :j, :Project).out(:BASED_AT, :l, :Location) },
|
|
28
|
+
parsed_knows_chain: -> { ctx.parse_match("MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person) RETURN a, b, c") },
|
|
28
29
|
knows_chain: -> { m.().node(:a, :Person).out(:KNOWS, :b, :Person).out(:KNOWS, :c, :Person) },
|
|
29
30
|
inbound: -> { m.().node(:l, :Location).in(:BASED_AT, :j, :Project).in(:WORKS_ON, :p, :Person) },
|
|
30
31
|
branch: -> { m.().node(:j, :Project).in(:WORKS_ON, :p, :Person).out(:BASED_AT, :l, :Location, from: :j) },
|
|
@@ -69,6 +70,15 @@ module Pglpg
|
|
|
69
70
|
out[:last_by_name] = element_data(m.().node(:p, :Person).order(p: { name: :desc }).first&.fetch(:p))
|
|
70
71
|
out[:find] = refs.map { element_data(ctx.find(_1)) }
|
|
71
72
|
out[:find_node] = %w[P1 P2 P3 H1].map { element_data(ctx.find_node(:Person, namespace: _1 == "H1" ? "hidden" : "visible", uid: _1)) }
|
|
73
|
+
out[:find_composite_node] = [["J1", "Apollo"], ["J2", "Gemini"], ["HJ", "Apollo"]].map do |uid, title|
|
|
74
|
+
element_data(ctx.find_node(:Project, uid:, title:))
|
|
75
|
+
end
|
|
76
|
+
persons = refs.select(&:node?).select { ctx.find(_1)&.key_label == :Person }
|
|
77
|
+
projects = refs.select(&:node?).select { ctx.find(_1)&.key_label == :Project }
|
|
78
|
+
out[:find_edge] = persons.product(projects).map do |person, project|
|
|
79
|
+
element_data(ctx.find_edge(:WORKS_ON, uid: "#{person.id}-#{project.id}", batch: 1))
|
|
80
|
+
end
|
|
81
|
+
out[:find_expired_edge] = element_data(ctx.find_edge(:WORKS_ON, uid: "expired", batch: 1)) if ctx.as_of.instance_of?(Date)
|
|
72
82
|
out.merge(observe_models(ctx, refs))
|
|
73
83
|
end
|
|
74
84
|
|
data/lib/pglpg/version.rb
CHANGED
data/lib/pglpg/writer.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Pglpg
|
|
|
11
11
|
# Scope-wide occupancy permission, distinct from write or read access.
|
|
12
12
|
KeyScopeRequest = Data.define(:context, :domain, :scope)
|
|
13
13
|
|
|
14
|
-
WriteRequest = Data.define(:viewer, :operation, :type, :target, :from, :to, :key, :properties, :attrs, :trusted)
|
|
14
|
+
WriteRequest = Data.define(:viewer, :operation, :type, :target, :from, :to, :key, :properties, :attrs, :trusted, :validity, :validity_supplied)
|
|
15
15
|
|
|
16
16
|
# Compiles writes into single statements. Visibility of the existing row,
|
|
17
17
|
# the policy's conditions on it and endpoint checks all run inside the
|
|
@@ -21,6 +21,7 @@ module Pglpg
|
|
|
21
21
|
attr_reader :context
|
|
22
22
|
|
|
23
23
|
def initialize(context, trusted:)
|
|
24
|
+
context.writable!
|
|
24
25
|
@context = context
|
|
25
26
|
@graph = context.graph
|
|
26
27
|
@trusted = trusted
|
|
@@ -28,14 +29,8 @@ module Pglpg
|
|
|
28
29
|
|
|
29
30
|
def insert_node(type, properties: {}, attrs: {})
|
|
30
31
|
node_type = concrete_node_type!(type)
|
|
31
|
-
props = properties
|
|
32
|
-
|
|
33
|
-
if props.key?(key.property)
|
|
34
|
-
raise ArgumentError, "#{node_type.name}.#{key.property} is a key and is generated" unless @trusted || key.scope
|
|
35
|
-
else
|
|
36
|
-
props[key.property] = key.generate(node_type.name)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
32
|
+
props = prepare_key(node_type, properties)
|
|
33
|
+
key = node_type.key
|
|
39
34
|
json = node_type.encode_properties(props)
|
|
40
35
|
cols = encode_attrs(@graph.type.node_columns, attrs, insert: true)
|
|
41
36
|
authorize!(:insert_node, node_type.name, properties: props, attrs:)
|
|
@@ -56,7 +51,7 @@ module Pglpg
|
|
|
56
51
|
@graph.check_ref!(ref, :node)
|
|
57
52
|
node = @context.find(ref) or raise WriteRejected
|
|
58
53
|
node_type = @graph.type.node_type!(node.key_label)
|
|
59
|
-
props =
|
|
54
|
+
props = node_type.normalize_properties(properties)
|
|
60
55
|
raise ArgumentError, "keys cannot be changed" if node_type.key && (props.keys & node_type.key.properties).any?
|
|
61
56
|
|
|
62
57
|
json = node_type.encode_properties(props, partial: true)
|
|
@@ -81,9 +76,9 @@ module Pglpg
|
|
|
81
76
|
node_type = concrete_node_type!(type)
|
|
82
77
|
node_key = node_type.key or raise ArgumentError, "#{node_type.name} has no key"
|
|
83
78
|
key = key.transform_keys(&:to_sym)
|
|
84
|
-
|
|
79
|
+
node_key.validate_values!(node_type, key)
|
|
85
80
|
|
|
86
|
-
props =
|
|
81
|
+
props = node_type.normalize_properties(properties)
|
|
87
82
|
raise ArgumentError, "pass the key in key:, not properties:" if (props.keys & node_key.properties).any?
|
|
88
83
|
|
|
89
84
|
props = props.merge(key)
|
|
@@ -94,11 +89,7 @@ module Pglpg
|
|
|
94
89
|
sql = new_sql
|
|
95
90
|
insert_node_values(sql, node_type, json, cols)
|
|
96
91
|
sql << " ON CONFLICT ("
|
|
97
|
-
node_key.
|
|
98
|
-
sql << ", " if i.positive?
|
|
99
|
-
sql << "(properties->>"
|
|
100
|
-
sql.literal(property) << ")"
|
|
101
|
-
end
|
|
92
|
+
sql << node_key.expressions(@graph.connection).join(", ")
|
|
102
93
|
sql << ") WHERE labels @> ARRAY["
|
|
103
94
|
sql.literal(node_key.declared_on) << "]::text[] DO UPDATE SET "
|
|
104
95
|
set_properties(sql, "tgt.properties", json, null_keys(props), excluded: true)
|
|
@@ -126,32 +117,50 @@ module Pglpg
|
|
|
126
117
|
true
|
|
127
118
|
end
|
|
128
119
|
|
|
129
|
-
def insert_edge(type, from:, to:, properties: {}, attrs: {})
|
|
120
|
+
def insert_edge(type, from:, to:, properties: {}, attrs: {}, valid_from: Temporal::OMITTED, valid_to: Temporal::OMITTED)
|
|
130
121
|
edge_type = @graph.type.edge_type!(type)
|
|
131
|
-
if edge_type.
|
|
132
|
-
raise ArgumentError, "#{edge_type.name} is unique per pair; write it through the trusted writer"
|
|
122
|
+
if edge_type.unique_tuple? && !@trusted
|
|
123
|
+
raise ArgumentError, "#{edge_type.name} is unique per pair or property tuple; write it through the trusted writer"
|
|
133
124
|
end
|
|
134
125
|
|
|
135
|
-
write_edge(:insert_edge, edge_type, from, to, properties, attrs)
|
|
126
|
+
write_edge(:insert_edge, edge_type, from, to, properties, attrs, valid_from, valid_to)
|
|
136
127
|
end
|
|
137
128
|
|
|
138
|
-
# Trusted only
|
|
139
|
-
|
|
129
|
+
# Trusted only. A declared edge key takes precedence as conflict arbiter;
|
|
130
|
+
# otherwise the endpoint/property tuple is used.
|
|
131
|
+
def upsert_edge(type, from:, to:, key: nil, properties: {}, attrs: {}, valid_from: Temporal::OMITTED, valid_to: Temporal::OMITTED)
|
|
140
132
|
trusted!(:upsert_edge)
|
|
141
133
|
edge_type = @graph.type.edge_type!(type)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
134
|
+
if edge_type.key
|
|
135
|
+
raise ArgumentError, "#{edge_type.name} requires key:" unless key.is_a?(Hash)
|
|
136
|
+
|
|
137
|
+
key = key.transform_keys(&:to_sym)
|
|
138
|
+
edge_type.key.validate_values!(edge_type, key)
|
|
139
|
+
properties = edge_type.normalize_properties(properties)
|
|
140
|
+
raise ArgumentError, "pass the key in key:, not properties:" if (properties.keys & edge_type.key.properties).any?
|
|
141
|
+
|
|
142
|
+
properties = properties.merge(key)
|
|
143
|
+
else
|
|
144
|
+
raise ArgumentError, "#{edge_type.name} has no property key" unless key.nil?
|
|
145
|
+
raise ArgumentError, "#{edge_type.name} is not unique; use insert_edge" unless edge_type.unique_tuple?
|
|
146
|
+
end
|
|
147
|
+
write_edge(:upsert_edge, edge_type, from, to, properties, attrs, valid_from, valid_to, key:)
|
|
145
148
|
end
|
|
146
149
|
|
|
147
|
-
def update_edge(ref, properties: {}, attrs: {})
|
|
150
|
+
def update_edge(ref, properties: {}, attrs: {}, valid_from: Temporal::OMITTED, valid_to: Temporal::OMITTED)
|
|
148
151
|
@graph.check_ref!(ref, :edge)
|
|
149
|
-
edge = @context.find(ref) or raise WriteRejected
|
|
152
|
+
edge = @graph.for(viewer: @context.viewer, as_of: :any).find(ref) or raise WriteRejected
|
|
150
153
|
edge_type = @graph.type.edge_type!(edge.type)
|
|
151
|
-
|
|
154
|
+
validity = Temporal.bounds(edge_type, valid_from, valid_to)
|
|
155
|
+
Temporal.validate_interval!(validity.fetch(:valid_from, edge.valid_from), validity.fetch(:valid_to, edge.valid_to))
|
|
156
|
+
props = edge_type.normalize_properties(properties)
|
|
157
|
+
if (props.keys & edge_type.immutable_properties).any?
|
|
158
|
+
raise ArgumentError, "key and uniqueness properties cannot be changed"
|
|
159
|
+
end
|
|
152
160
|
json = edge_type.encode_properties(props, partial: true)
|
|
153
161
|
cols = encode_attrs(@graph.type.edge_columns, attrs, insert: false)
|
|
154
|
-
|
|
162
|
+
cols += validity_columns(validity)
|
|
163
|
+
request = authorize!(:update_edge, edge_type.name, target: ref, properties: props, attrs:, validity:)
|
|
155
164
|
|
|
156
165
|
sql = new_sql
|
|
157
166
|
sql << "UPDATE " << table(@graph.edges_table) << " AS tgt SET "
|
|
@@ -165,7 +174,7 @@ module Pglpg
|
|
|
165
174
|
|
|
166
175
|
def delete_edge(ref)
|
|
167
176
|
@graph.check_ref!(ref, :edge)
|
|
168
|
-
edge = @context.find(ref) or raise WriteRejected
|
|
177
|
+
edge = @graph.for(viewer: @context.viewer, as_of: :any).find(ref) or raise WriteRejected
|
|
169
178
|
request = authorize!(:delete_edge, edge.type, target: ref)
|
|
170
179
|
|
|
171
180
|
sql = new_sql
|
|
@@ -179,7 +188,8 @@ module Pglpg
|
|
|
179
188
|
|
|
180
189
|
private
|
|
181
190
|
|
|
182
|
-
def write_edge(operation, edge_type, from, to, properties, attrs)
|
|
191
|
+
def write_edge(operation, edge_type, from, to, properties, attrs, valid_from, valid_to, key: nil)
|
|
192
|
+
validity = Temporal.bounds(edge_type, valid_from, valid_to, required: true)
|
|
183
193
|
@graph.check_ref!(from, :node)
|
|
184
194
|
@graph.check_ref!(to, :node)
|
|
185
195
|
if edge_type.symmetric?
|
|
@@ -190,10 +200,12 @@ module Pglpg
|
|
|
190
200
|
# Stored in canonical orientation, so either order names the same pair.
|
|
191
201
|
from, to = to, from if from.id > to.id
|
|
192
202
|
end
|
|
193
|
-
props = properties
|
|
203
|
+
props = prepare_key(edge_type, properties)
|
|
194
204
|
json = edge_type.encode_properties(props)
|
|
195
205
|
cols = encode_attrs(@graph.type.edge_columns, attrs, insert: true)
|
|
196
|
-
|
|
206
|
+
update_cols = cols + validity_columns(validity)
|
|
207
|
+
cols += validity_columns(edge_type.temporal? ? { valid_from: nil, valid_to: nil }.merge(validity) : {})
|
|
208
|
+
request = authorize!(operation, edge_type.name, from:, to:, key:, properties: props, attrs:, validity:)
|
|
197
209
|
|
|
198
210
|
sql = new_sql
|
|
199
211
|
sql << "INSERT INTO " << table(@graph.edges_table) << " AS tgt (type, source_id, target_id, properties"
|
|
@@ -221,17 +233,49 @@ module Pglpg
|
|
|
221
233
|
# Endpoints stay locked against concurrent change until commit.
|
|
222
234
|
sql << " FOR SHARE OF s, t"
|
|
223
235
|
if operation == :upsert_edge
|
|
224
|
-
|
|
236
|
+
expressions = if edge_type.key
|
|
237
|
+
edge_type.key.expressions(@graph.connection)
|
|
238
|
+
else
|
|
239
|
+
["source_id", "target_id", *(edge_type.pair_key&.expressions(@graph.connection) || [])]
|
|
240
|
+
end
|
|
241
|
+
sql << " ON CONFLICT (" << expressions.join(", ") << ") WHERE type = "
|
|
225
242
|
sql.literal(edge_type.name) << " DO UPDATE SET "
|
|
226
243
|
set_properties(sql, "tgt.properties", json, null_keys(props), excluded: true)
|
|
227
|
-
set_columns(sql,
|
|
228
|
-
|
|
244
|
+
set_columns(sql, update_cols, excluded: true)
|
|
245
|
+
# A property key must never redirect an existing relationship. Pair
|
|
246
|
+
# properties are immutable too, including when a global key arbitrates.
|
|
247
|
+
sql << " WHERE tgt.source_id = EXCLUDED.source_id AND tgt.target_id = EXCLUDED.target_id"
|
|
248
|
+
edge_type.unique_pair_properties.each do |property|
|
|
249
|
+
sql << " AND tgt.properties->"
|
|
250
|
+
sql.literal(property) << " = EXCLUDED.properties->"
|
|
251
|
+
sql.literal(property)
|
|
252
|
+
end
|
|
253
|
+
sql << " AND "
|
|
229
254
|
existing_row(sql, :edge, request)
|
|
230
255
|
end
|
|
231
256
|
sql << " RETURNING id"
|
|
232
257
|
ref(:edge, write(sql))
|
|
233
258
|
end
|
|
234
259
|
|
|
260
|
+
def prepare_key(type, properties)
|
|
261
|
+
props = type.normalize_properties(properties)
|
|
262
|
+
key = type.key
|
|
263
|
+
return props unless key
|
|
264
|
+
unless key.generated?
|
|
265
|
+
raise ArgumentError, "#{type.name} has a caller-chosen key; use the trusted writer" unless @trusted
|
|
266
|
+
|
|
267
|
+
return props
|
|
268
|
+
end
|
|
269
|
+
if props.key?(key.property)
|
|
270
|
+
unless @trusted || (type.is_a?(Schema::NodeType) && key.scope)
|
|
271
|
+
raise ArgumentError, "#{type.name}.#{key.property} is a key and is generated"
|
|
272
|
+
end
|
|
273
|
+
else
|
|
274
|
+
props[key.property] = key.generate(type.name)
|
|
275
|
+
end
|
|
276
|
+
props
|
|
277
|
+
end
|
|
278
|
+
|
|
235
279
|
def endpoint_labels(sql, source_label, target_label)
|
|
236
280
|
sql << "(s.labels @> "
|
|
237
281
|
sql.bind_text_array([source_label]) << " AND t.labels @> "
|
|
@@ -300,12 +344,17 @@ module Pglpg
|
|
|
300
344
|
end
|
|
301
345
|
end
|
|
302
346
|
|
|
347
|
+
def validity_columns(validity)
|
|
348
|
+
validity.map { |name, value| [Schema::Column.new(name, :date), value&.iso8601] }
|
|
349
|
+
end
|
|
350
|
+
|
|
303
351
|
def null_keys(props) = props.select { |_, v| v.nil? }.keys.map(&:to_s)
|
|
304
352
|
|
|
305
|
-
def authorize!(operation, type, target: nil, from: nil, to: nil, key: nil, properties: {}, attrs: {})
|
|
353
|
+
def authorize!(operation, type, target: nil, from: nil, to: nil, key: nil, properties: {}, attrs: {}, validity: {}.freeze)
|
|
306
354
|
request = WriteRequest.new(viewer: @context.viewer, operation:, type: type.to_sym, target:, from:, to:, key:,
|
|
307
355
|
properties: properties.freeze, attrs: attrs.transform_keys(&:to_sym).freeze,
|
|
308
|
-
trusted: @trusted
|
|
356
|
+
trusted: @trusted, validity:,
|
|
357
|
+
validity_supplied: Temporal::NAMES.to_h { [_1, validity.key?(_1)] }.freeze)
|
|
309
358
|
raise WriteRejected unless @graph.write_policy.authorize?(request) == true
|
|
310
359
|
|
|
311
360
|
request
|
|
@@ -348,7 +397,7 @@ module Pglpg
|
|
|
348
397
|
rescue ActiveRecord::StatementInvalid => e
|
|
349
398
|
case e.cause
|
|
350
399
|
when PG::UniqueViolation, PG::ForeignKeyViolation, PG::RestrictViolation then raise WriteRejected, cause: nil
|
|
351
|
-
when PG::NotNullViolation, PG::CheckViolation then raise ValidationError,
|
|
400
|
+
when PG::NotNullViolation, PG::CheckViolation then raise ValidationError, "write violates schema constraints", cause: nil
|
|
352
401
|
else raise
|
|
353
402
|
end
|
|
354
403
|
end
|
data/lib/pglpg.rb
CHANGED
|
@@ -6,8 +6,10 @@ require "json"
|
|
|
6
6
|
require_relative "pglpg/version"
|
|
7
7
|
require_relative "pglpg/errors"
|
|
8
8
|
require_relative "pglpg/configuration"
|
|
9
|
+
require_relative "pglpg/temporal"
|
|
9
10
|
require_relative "pglpg/schema/property"
|
|
10
11
|
require_relative "pglpg/schema/element_type"
|
|
12
|
+
require_relative "pglpg/schema/key"
|
|
11
13
|
require_relative "pglpg/schema/node_type"
|
|
12
14
|
require_relative "pglpg/schema/edge_type"
|
|
13
15
|
require_relative "pglpg/schema/column"
|
|
@@ -21,6 +23,7 @@ require_relative "pglpg/graph"
|
|
|
21
23
|
require_relative "pglpg/context"
|
|
22
24
|
require_relative "pglpg/writer"
|
|
23
25
|
require_relative "pglpg/query"
|
|
26
|
+
require_relative "pglpg/match_parser"
|
|
24
27
|
require_relative "pglpg/reachability"
|
|
25
28
|
|
|
26
29
|
# Labeled property graphs stored in plain PostgreSQL tables.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pglpg
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anton Fefilov
|
|
@@ -54,6 +54,7 @@ files:
|
|
|
54
54
|
- lib/pglpg/element.rb
|
|
55
55
|
- lib/pglpg/errors.rb
|
|
56
56
|
- lib/pglpg/graph.rb
|
|
57
|
+
- lib/pglpg/match_parser.rb
|
|
57
58
|
- lib/pglpg/model.rb
|
|
58
59
|
- lib/pglpg/model/base.rb
|
|
59
60
|
- lib/pglpg/model/edge.rb
|
|
@@ -66,10 +67,12 @@ files:
|
|
|
66
67
|
- lib/pglpg/schema/edge_type.rb
|
|
67
68
|
- lib/pglpg/schema/element_type.rb
|
|
68
69
|
- lib/pglpg/schema/graph_type.rb
|
|
70
|
+
- lib/pglpg/schema/key.rb
|
|
69
71
|
- lib/pglpg/schema/node_type.rb
|
|
70
72
|
- lib/pglpg/schema/property.rb
|
|
71
73
|
- lib/pglpg/sql.rb
|
|
72
74
|
- lib/pglpg/storage.rb
|
|
75
|
+
- lib/pglpg/temporal.rb
|
|
73
76
|
- lib/pglpg/testing.rb
|
|
74
77
|
- lib/pglpg/testing/leak_examples.rb
|
|
75
78
|
- lib/pglpg/version.rb
|