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.
@@ -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
- let(:pglpg_leak_graph) { leak_graph.call(pglpg_visibility, pglpg_columns) }
63
- let(:pglpg_seed_graph) { leak_graph.call(Pglpg::Testing::SeeAll, pglpg_columns) }
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 = ->(uid, name, rank, attrs) { seed.insert_node(:Person, properties: { uid:, name:, rank:, namespace: attrs == hid ? "hidden" : "visible" }, attrs:) }
71
- node = ->(type, uid, attrs, **props) { seed.insert_node(type, properties: { uid:, **props }, attrs:) }
72
- edge = ->(type, from, to, attrs, **props) { seed.insert_edge(type, from:, to:, properties: props, attrs:) }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pglpg
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
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.transform_keys(&:to_sym)
32
- if (key = node_type.key)
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 = properties.transform_keys(&:to_sym)
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
- raise ArgumentError, "#{node_type.name} is upserted by #{node_key.properties.inspect}" unless key.keys.sort == node_key.properties.sort
79
+ node_key.validate_values!(node_type, key)
85
80
 
86
- props = properties.transform_keys(&:to_sym)
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.properties.each_with_index do |property, i|
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.unique_pair? && !@trusted
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, for edge types declared unique: :pair.
139
- def upsert_edge(type, from:, to:, properties: {}, attrs: {})
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
- raise ArgumentError, "#{edge_type.name} is not unique per pair; use insert_edge" unless edge_type.unique_pair?
143
-
144
- write_edge(:upsert_edge, edge_type, from, to, properties, attrs)
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
- props = properties.transform_keys(&:to_sym)
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
- request = authorize!(:update_edge, edge_type.name, target: ref, properties: props, attrs:)
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.transform_keys(&:to_sym)
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
- request = authorize!(operation, edge_type.name, from:, to:, properties: props, attrs:)
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
- sql << " ON CONFLICT (source_id, target_id) WHERE type = "
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, cols, excluded: true)
228
- sql << " WHERE "
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, e.message
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.2.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