pglpg 0.1.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 +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE +21 -0
- data/README.md +153 -0
- data/lib/pglpg/condition.rb +33 -0
- data/lib/pglpg/configuration.rb +48 -0
- data/lib/pglpg/context.rb +120 -0
- data/lib/pglpg/element.rb +62 -0
- data/lib/pglpg/errors.rb +40 -0
- data/lib/pglpg/graph.rb +132 -0
- data/lib/pglpg/query.rb +411 -0
- data/lib/pglpg/ref.rb +25 -0
- data/lib/pglpg/schema/column.rb +38 -0
- data/lib/pglpg/schema/edge_type.rb +35 -0
- data/lib/pglpg/schema/element_type.rb +65 -0
- data/lib/pglpg/schema/graph_type.rb +100 -0
- data/lib/pglpg/schema/node_type.rb +92 -0
- data/lib/pglpg/schema/property.rb +123 -0
- data/lib/pglpg/sql.rb +75 -0
- data/lib/pglpg/storage.rb +208 -0
- data/lib/pglpg/testing/leak_examples.rb +121 -0
- data/lib/pglpg/testing.rb +64 -0
- data/lib/pglpg/version.rb +5 -0
- data/lib/pglpg/writer.rb +329 -0
- data/lib/pglpg.rb +27 -0
- metadata +95 -0
data/lib/pglpg/writer.rb
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pglpg
|
|
4
|
+
# What a write asks for, as passed to the write policy.
|
|
5
|
+
# operation: :insert_node, :update_node, :upsert_node, :delete_node,
|
|
6
|
+
# :insert_edge, :update_edge, :upsert_edge or :delete_edge
|
|
7
|
+
# type: the node or edge type
|
|
8
|
+
# target: the Ref being changed (update and delete), else nil
|
|
9
|
+
# from, to: endpoint Refs for edge inserts and upserts
|
|
10
|
+
# key: the key for upserts
|
|
11
|
+
WriteRequest = Data.define(:viewer, :operation, :type, :target, :from, :to, :key, :properties, :attrs, :trusted)
|
|
12
|
+
|
|
13
|
+
# Compiles writes into single statements. Visibility of the existing row,
|
|
14
|
+
# the policy's conditions on it and endpoint checks all run inside the
|
|
15
|
+
# statement, so they hold against the row the statement actually touches,
|
|
16
|
+
# even under concurrent writes.
|
|
17
|
+
class Writer
|
|
18
|
+
def initialize(context, trusted:)
|
|
19
|
+
@context = context
|
|
20
|
+
@graph = context.graph
|
|
21
|
+
@trusted = trusted
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def insert_node(type, properties: {}, attrs: {})
|
|
25
|
+
node_type = concrete_node_type!(type)
|
|
26
|
+
props = properties.transform_keys(&:to_sym)
|
|
27
|
+
if (key = node_type.key)
|
|
28
|
+
if props.key?(key.property)
|
|
29
|
+
raise ArgumentError, "#{node_type.name}.#{key.property} is a key and is generated" unless @trusted
|
|
30
|
+
else
|
|
31
|
+
props[key.property] = key.generate(node_type.name)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
json = node_type.encode_properties(props)
|
|
35
|
+
cols = encode_attrs(@graph.type.node_columns, attrs, insert: true)
|
|
36
|
+
authorize!(:insert_node, node_type.name, properties: props, attrs:)
|
|
37
|
+
|
|
38
|
+
sql = new_sql
|
|
39
|
+
insert_node_values(sql, node_type, json, cols)
|
|
40
|
+
sql << " RETURNING id"
|
|
41
|
+
ref(:node, write(sql))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def update_node(ref, properties: {}, attrs: {})
|
|
45
|
+
@graph.check_ref!(ref, :node)
|
|
46
|
+
node = @context.find(ref) or raise WriteRejected
|
|
47
|
+
node_type = @graph.type.node_type!(node.key_label)
|
|
48
|
+
props = properties.transform_keys(&:to_sym)
|
|
49
|
+
raise ArgumentError, "keys cannot be changed" if node_type.key && props.key?(node_type.key.property)
|
|
50
|
+
|
|
51
|
+
json = node_type.encode_properties(props, partial: true)
|
|
52
|
+
cols = encode_attrs(@graph.type.node_columns, attrs, insert: false)
|
|
53
|
+
request = authorize!(:update_node, node_type.name, target: ref, properties: props, attrs:)
|
|
54
|
+
|
|
55
|
+
sql = new_sql
|
|
56
|
+
sql << "UPDATE " << table(@graph.nodes_table) << " AS tgt SET "
|
|
57
|
+
set_properties(sql, "tgt.properties", json, null_keys(props))
|
|
58
|
+
set_columns(sql, cols)
|
|
59
|
+
sql << " WHERE tgt.id = "
|
|
60
|
+
sql.bind(ref.id, "bigint") << " AND tgt.key_label = "
|
|
61
|
+
sql.bind(node_type.name.to_s, "text") << " AND "
|
|
62
|
+
existing_row(sql, :node, request)
|
|
63
|
+
sql << " RETURNING tgt.id"
|
|
64
|
+
self.ref(:node, write(sql))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Trusted only. Inserts, or updates the node of the same type with this key.
|
|
68
|
+
def upsert_node(type, key:, properties: {}, attrs: {})
|
|
69
|
+
trusted!(:upsert_node)
|
|
70
|
+
node_type = concrete_node_type!(type)
|
|
71
|
+
node_key = node_type.key or raise ArgumentError, "#{node_type.name} has no key"
|
|
72
|
+
key = key.transform_keys(&:to_sym)
|
|
73
|
+
raise ArgumentError, "#{node_type.name} is upserted by #{node_key.property.inspect}" unless key.keys == [node_key.property]
|
|
74
|
+
|
|
75
|
+
props = properties.transform_keys(&:to_sym)
|
|
76
|
+
raise ArgumentError, "pass the key in key:, not properties:" if props.key?(node_key.property)
|
|
77
|
+
|
|
78
|
+
props = props.merge(key)
|
|
79
|
+
json = node_type.encode_properties(props)
|
|
80
|
+
cols = encode_attrs(@graph.type.node_columns, attrs, insert: true)
|
|
81
|
+
request = authorize!(:upsert_node, node_type.name, key:, properties: props, attrs:)
|
|
82
|
+
|
|
83
|
+
sql = new_sql
|
|
84
|
+
insert_node_values(sql, node_type, json, cols)
|
|
85
|
+
sql << " ON CONFLICT ((properties->>"
|
|
86
|
+
sql.literal(node_key.property) << ")) WHERE labels @> ARRAY["
|
|
87
|
+
sql.literal(node_key.declared_on) << "]::text[] DO UPDATE SET "
|
|
88
|
+
set_properties(sql, "tgt.properties", json, null_keys(props), excluded: true)
|
|
89
|
+
set_columns(sql, cols, excluded: true)
|
|
90
|
+
sql << " WHERE tgt.key_label = EXCLUDED.key_label AND "
|
|
91
|
+
existing_row(sql, :node, request)
|
|
92
|
+
sql << " RETURNING id"
|
|
93
|
+
ref(:node, write(sql))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Trusted only. Nodes that still have edges cannot be deleted.
|
|
97
|
+
def delete_node(ref)
|
|
98
|
+
trusted!(:delete_node)
|
|
99
|
+
@graph.check_ref!(ref, :node)
|
|
100
|
+
node = @context.find(ref) or raise WriteRejected
|
|
101
|
+
request = authorize!(:delete_node, node.key_label, target: ref)
|
|
102
|
+
|
|
103
|
+
sql = new_sql
|
|
104
|
+
sql << "DELETE FROM " << table(@graph.nodes_table) << " AS tgt WHERE tgt.id = "
|
|
105
|
+
sql.bind(ref.id, "bigint") << " AND tgt.key_label = "
|
|
106
|
+
sql.bind(node.key_label.to_s, "text") << " AND "
|
|
107
|
+
existing_row(sql, :node, request)
|
|
108
|
+
sql << " RETURNING tgt.id"
|
|
109
|
+
write(sql)
|
|
110
|
+
true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def insert_edge(type, from:, to:, properties: {}, attrs: {})
|
|
114
|
+
edge_type = @graph.type.edge_type!(type)
|
|
115
|
+
if edge_type.unique_pair? && !@trusted
|
|
116
|
+
raise ArgumentError, "#{edge_type.name} is unique per pair; write it through the trusted writer"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
write_edge(:insert_edge, edge_type, from, to, properties, attrs)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Trusted only, for edge types declared unique: :pair.
|
|
123
|
+
def upsert_edge(type, from:, to:, properties: {}, attrs: {})
|
|
124
|
+
trusted!(:upsert_edge)
|
|
125
|
+
edge_type = @graph.type.edge_type!(type)
|
|
126
|
+
raise ArgumentError, "#{edge_type.name} is not unique per pair; use insert_edge" unless edge_type.unique_pair?
|
|
127
|
+
|
|
128
|
+
write_edge(:upsert_edge, edge_type, from, to, properties, attrs)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def update_edge(ref, properties: {}, attrs: {})
|
|
132
|
+
@graph.check_ref!(ref, :edge)
|
|
133
|
+
edge = @context.find(ref) or raise WriteRejected
|
|
134
|
+
edge_type = @graph.type.edge_type!(edge.type)
|
|
135
|
+
props = properties.transform_keys(&:to_sym)
|
|
136
|
+
json = edge_type.encode_properties(props, partial: true)
|
|
137
|
+
cols = encode_attrs(@graph.type.edge_columns, attrs, insert: false)
|
|
138
|
+
request = authorize!(:update_edge, edge_type.name, target: ref, properties: props, attrs:)
|
|
139
|
+
|
|
140
|
+
sql = new_sql
|
|
141
|
+
sql << "UPDATE " << table(@graph.edges_table) << " AS tgt SET "
|
|
142
|
+
set_properties(sql, "tgt.properties", json, null_keys(props))
|
|
143
|
+
set_columns(sql, cols)
|
|
144
|
+
sql << " FROM " << table(@graph.nodes_table) << " AS s, " << table(@graph.nodes_table) << " AS t"
|
|
145
|
+
edge_row_condition(sql, ref, edge_type, request)
|
|
146
|
+
sql << " RETURNING tgt.id"
|
|
147
|
+
self.ref(:edge, write(sql))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def delete_edge(ref)
|
|
151
|
+
@graph.check_ref!(ref, :edge)
|
|
152
|
+
edge = @context.find(ref) or raise WriteRejected
|
|
153
|
+
request = authorize!(:delete_edge, edge.type, target: ref)
|
|
154
|
+
|
|
155
|
+
sql = new_sql
|
|
156
|
+
sql << "DELETE FROM " << table(@graph.edges_table) << " AS tgt USING "
|
|
157
|
+
sql << table(@graph.nodes_table) << " AS s, " << table(@graph.nodes_table) << " AS t"
|
|
158
|
+
edge_row_condition(sql, ref, @graph.type.edge_type!(edge.type), request)
|
|
159
|
+
sql << " RETURNING tgt.id"
|
|
160
|
+
write(sql)
|
|
161
|
+
true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def write_edge(operation, edge_type, from, to, properties, attrs)
|
|
167
|
+
@graph.check_ref!(from, :node)
|
|
168
|
+
@graph.check_ref!(to, :node)
|
|
169
|
+
props = properties.transform_keys(&:to_sym)
|
|
170
|
+
json = edge_type.encode_properties(props)
|
|
171
|
+
cols = encode_attrs(@graph.type.edge_columns, attrs, insert: true)
|
|
172
|
+
request = authorize!(operation, edge_type.name, from:, to:, properties: props, attrs:)
|
|
173
|
+
|
|
174
|
+
sql = new_sql
|
|
175
|
+
sql << "INSERT INTO " << table(@graph.edges_table) << " AS tgt (type, source_id, target_id, properties"
|
|
176
|
+
cols.each { |c, _| sql << ", " << ident(c.name) }
|
|
177
|
+
sql << ") SELECT "
|
|
178
|
+
sql.bind(edge_type.name.to_s, "text") << ", s.id, t.id, "
|
|
179
|
+
sql.bind_json(json)
|
|
180
|
+
cols.each do |c, v|
|
|
181
|
+
sql << ", "
|
|
182
|
+
sql.bind(v, c.sql_type(@graph.connection))
|
|
183
|
+
end
|
|
184
|
+
sql << " FROM " << table(@graph.nodes_table) << " AS s, " << table(@graph.nodes_table) << " AS t WHERE s.id = "
|
|
185
|
+
sql.bind(from.id, "bigint") << " AND t.id = "
|
|
186
|
+
sql.bind(to.id, "bigint") << " AND s.labels @> "
|
|
187
|
+
sql.bind_text_array([edge_type.from]) << " AND t.labels @> "
|
|
188
|
+
sql.bind_text_array([edge_type.to]) << " AND "
|
|
189
|
+
@context.append_node_visible(sql, "s")
|
|
190
|
+
sql << " AND "
|
|
191
|
+
@context.append_node_visible(sql, "t")
|
|
192
|
+
# Endpoints stay locked against concurrent change until commit.
|
|
193
|
+
sql << " FOR SHARE OF s, t"
|
|
194
|
+
if operation == :upsert_edge
|
|
195
|
+
sql << " ON CONFLICT (source_id, target_id) WHERE type = "
|
|
196
|
+
sql.literal(edge_type.name) << " DO UPDATE SET "
|
|
197
|
+
set_properties(sql, "tgt.properties", json, null_keys(props), excluded: true)
|
|
198
|
+
set_columns(sql, cols, excluded: true)
|
|
199
|
+
sql << " WHERE "
|
|
200
|
+
existing_row(sql, :edge, request)
|
|
201
|
+
end
|
|
202
|
+
sql << " RETURNING id"
|
|
203
|
+
ref(:edge, write(sql))
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def insert_node_values(sql, node_type, json, cols)
|
|
207
|
+
sql << "INSERT INTO " << table(@graph.nodes_table) << " AS tgt (key_label, labels, properties"
|
|
208
|
+
cols.each { |c, _| sql << ", " << ident(c.name) }
|
|
209
|
+
sql << ") VALUES ("
|
|
210
|
+
sql.bind(node_type.name.to_s, "text") << ", "
|
|
211
|
+
sql.bind_text_array(node_type.labels) << ", "
|
|
212
|
+
sql.bind_json(json)
|
|
213
|
+
cols.each do |c, v|
|
|
214
|
+
sql << ", "
|
|
215
|
+
sql.bind(v, c.sql_type(@graph.connection))
|
|
216
|
+
end
|
|
217
|
+
sql << ")"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# The edge row and both endpoints must be visible, and the policy's
|
|
221
|
+
# conditions on the stored edge must hold.
|
|
222
|
+
def edge_row_condition(sql, ref, edge_type, request)
|
|
223
|
+
sql << " WHERE tgt.id = "
|
|
224
|
+
sql.bind(ref.id, "bigint") << " AND tgt.type = "
|
|
225
|
+
sql.bind(edge_type.name.to_s, "text")
|
|
226
|
+
sql << " AND s.id = tgt.source_id AND t.id = tgt.target_id AND "
|
|
227
|
+
@context.append_node_visible(sql, "s")
|
|
228
|
+
sql << " AND "
|
|
229
|
+
@context.append_node_visible(sql, "t")
|
|
230
|
+
sql << " AND "
|
|
231
|
+
existing_row(sql, :edge, request)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Visibility of the stored row "tgt" plus the policy's optional
|
|
235
|
+
# conditions on it (allowed old-to-new transitions).
|
|
236
|
+
def existing_row(sql, kind, request)
|
|
237
|
+
kind == :node ? @context.append_node_visible(sql, "tgt") : @context.append_edge_visible(sql, "tgt")
|
|
238
|
+
policy = @graph.write_policy
|
|
239
|
+
return unless policy.respond_to?(:existing)
|
|
240
|
+
|
|
241
|
+
condition = policy.existing(Arel::Table.new("tgt"), request)
|
|
242
|
+
return if condition.nil?
|
|
243
|
+
unless condition.is_a?(Arel::Nodes::Node) || condition.is_a?(Arel::Nodes::SqlLiteral)
|
|
244
|
+
raise ConfigurationError, "write policy returned #{condition.class}, expected an Arel condition"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
sql << " AND ("
|
|
248
|
+
sql.arel(condition)
|
|
249
|
+
sql << ")"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# properties = (old || new) - removed keys
|
|
253
|
+
def set_properties(sql, current, json, removed, excluded: false)
|
|
254
|
+
sql << "properties = (" << current << " || "
|
|
255
|
+
excluded ? sql << "EXCLUDED.properties" : sql.bind_json(json)
|
|
256
|
+
sql << ") - "
|
|
257
|
+
sql.bind_text_array(removed)
|
|
258
|
+
sql << ", updated_at = now()"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def set_columns(sql, cols, excluded: false)
|
|
262
|
+
cols.each do |c, v|
|
|
263
|
+
sql << ", " << ident(c.name) << " = "
|
|
264
|
+
excluded ? sql << "EXCLUDED." << ident(c.name) : sql.bind(v, c.sql_type(@graph.connection))
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def null_keys(props) = props.select { |_, v| v.nil? }.keys.map(&:to_s)
|
|
269
|
+
|
|
270
|
+
def authorize!(operation, type, target: nil, from: nil, to: nil, key: nil, properties: {}, attrs: {})
|
|
271
|
+
request = WriteRequest.new(viewer: @context.viewer, operation:, type: type.to_sym, target:, from:, to:, key:,
|
|
272
|
+
properties: properties.freeze, attrs: attrs.transform_keys(&:to_sym).freeze,
|
|
273
|
+
trusted: @trusted)
|
|
274
|
+
raise WriteRejected unless @graph.write_policy.authorize?(request) == true
|
|
275
|
+
|
|
276
|
+
request
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def trusted!(operation)
|
|
280
|
+
raise NotTrusted, "#{operation} needs the trusted writer" unless @trusted
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def concrete_node_type!(type)
|
|
284
|
+
node_type = @graph.type.node_type!(type)
|
|
285
|
+
raise ValidationError, "#{node_type.name} is abstract" if node_type.abstract?
|
|
286
|
+
|
|
287
|
+
node_type
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def encode_attrs(columns, attrs, insert:)
|
|
291
|
+
attrs = (attrs || {}).transform_keys(&:to_sym)
|
|
292
|
+
unknown = attrs.keys - columns.keys
|
|
293
|
+
raise ValidationError, "unknown column#{"s" if unknown.size > 1} #{unknown.join(", ")}" if unknown.any?
|
|
294
|
+
|
|
295
|
+
columns.each_value do |c|
|
|
296
|
+
raise ValidationError, "#{c.name} is required" if insert && c.required? && attrs[c.name].nil?
|
|
297
|
+
raise ValidationError, "#{c.name} cannot be null" if attrs.key?(c.name) && attrs[c.name].nil? && !c.null?
|
|
298
|
+
end
|
|
299
|
+
attrs.map { |name, v| [columns.fetch(name), v] }
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Runs a write in a savepoint, so a refusal never aborts the caller's
|
|
303
|
+
# transaction. Zero rows means the statement's own checks refused it.
|
|
304
|
+
def write(sql)
|
|
305
|
+
id = @graph.with_connection do |c|
|
|
306
|
+
c.transaction(requires_new: true) do
|
|
307
|
+
c.exec_query(sql.to_s, "Pglpg", sql.binds).rows.first&.first
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
raise WriteRejected if id.nil?
|
|
311
|
+
|
|
312
|
+
id.to_i
|
|
313
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
314
|
+
case e.cause
|
|
315
|
+
when PG::UniqueViolation, PG::ForeignKeyViolation, PG::RestrictViolation then raise WriteRejected
|
|
316
|
+
when PG::NotNullViolation, PG::CheckViolation then raise ValidationError, e.message
|
|
317
|
+
else raise
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def ref(kind, id) = @graph.ref(kind, id)
|
|
322
|
+
|
|
323
|
+
def new_sql = Sql.new(@graph.connection)
|
|
324
|
+
|
|
325
|
+
def table(name) = @graph.connection.quote_table_name(name)
|
|
326
|
+
|
|
327
|
+
def ident(name) = @graph.connection.quote_column_name(name)
|
|
328
|
+
end
|
|
329
|
+
end
|
data/lib/pglpg.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "pglpg/version"
|
|
7
|
+
require_relative "pglpg/errors"
|
|
8
|
+
require_relative "pglpg/configuration"
|
|
9
|
+
require_relative "pglpg/schema/property"
|
|
10
|
+
require_relative "pglpg/schema/element_type"
|
|
11
|
+
require_relative "pglpg/schema/node_type"
|
|
12
|
+
require_relative "pglpg/schema/edge_type"
|
|
13
|
+
require_relative "pglpg/schema/column"
|
|
14
|
+
require_relative "pglpg/schema/graph_type"
|
|
15
|
+
require_relative "pglpg/ref"
|
|
16
|
+
require_relative "pglpg/element"
|
|
17
|
+
require_relative "pglpg/condition"
|
|
18
|
+
require_relative "pglpg/sql"
|
|
19
|
+
require_relative "pglpg/storage"
|
|
20
|
+
require_relative "pglpg/graph"
|
|
21
|
+
require_relative "pglpg/context"
|
|
22
|
+
require_relative "pglpg/writer"
|
|
23
|
+
require_relative "pglpg/query"
|
|
24
|
+
|
|
25
|
+
# Labeled property graphs stored in plain PostgreSQL tables.
|
|
26
|
+
module Pglpg
|
|
27
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pglpg
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Anton Fefilov
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: pg
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.5'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.5'
|
|
40
|
+
description: A Ruby library for labeled property graphs (ISO GQL model, PG-Schema
|
|
41
|
+
/ PG-Keys) stored in plain PostgreSQL tables, with a visibility policy applied to
|
|
42
|
+
every node and edge on every hop.
|
|
43
|
+
executables: []
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- CHANGELOG.md
|
|
48
|
+
- LICENSE
|
|
49
|
+
- README.md
|
|
50
|
+
- lib/pglpg.rb
|
|
51
|
+
- lib/pglpg/condition.rb
|
|
52
|
+
- lib/pglpg/configuration.rb
|
|
53
|
+
- lib/pglpg/context.rb
|
|
54
|
+
- lib/pglpg/element.rb
|
|
55
|
+
- lib/pglpg/errors.rb
|
|
56
|
+
- lib/pglpg/graph.rb
|
|
57
|
+
- lib/pglpg/query.rb
|
|
58
|
+
- lib/pglpg/ref.rb
|
|
59
|
+
- lib/pglpg/schema/column.rb
|
|
60
|
+
- lib/pglpg/schema/edge_type.rb
|
|
61
|
+
- lib/pglpg/schema/element_type.rb
|
|
62
|
+
- lib/pglpg/schema/graph_type.rb
|
|
63
|
+
- lib/pglpg/schema/node_type.rb
|
|
64
|
+
- lib/pglpg/schema/property.rb
|
|
65
|
+
- lib/pglpg/sql.rb
|
|
66
|
+
- lib/pglpg/storage.rb
|
|
67
|
+
- lib/pglpg/testing.rb
|
|
68
|
+
- lib/pglpg/testing/leak_examples.rb
|
|
69
|
+
- lib/pglpg/version.rb
|
|
70
|
+
- lib/pglpg/writer.rb
|
|
71
|
+
homepage: https://github.com/valamonai/pglpg
|
|
72
|
+
licenses:
|
|
73
|
+
- MIT
|
|
74
|
+
metadata:
|
|
75
|
+
source_code_uri: https://github.com/valamonai/pglpg
|
|
76
|
+
changelog_uri: https://github.com/valamonai/pglpg/blob/master/CHANGELOG.md
|
|
77
|
+
rubygems_mfa_required: 'true'
|
|
78
|
+
rdoc_options: []
|
|
79
|
+
require_paths:
|
|
80
|
+
- lib
|
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '4.0'
|
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '0'
|
|
91
|
+
requirements: []
|
|
92
|
+
rubygems_version: 4.0.17
|
|
93
|
+
specification_version: 4
|
|
94
|
+
summary: Labeled property graphs stored in plain PostgreSQL tables
|
|
95
|
+
test_files: []
|