flockdb 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -103,6 +103,28 @@ module Flock
103
103
  ::Thrift::Struct.generate_accessors self
104
104
  end
105
105
 
106
+ class Metadata
107
+ include ::Thrift::Struct, ::Thrift::Struct_Union
108
+ SOURCE_ID = 1
109
+ STATE_ID = 2
110
+ COUNT = 3
111
+ UPDATED_AT = 4
112
+
113
+ FIELDS = {
114
+ SOURCE_ID => {:type => ::Thrift::Types::I64, :name => 'source_id'},
115
+ STATE_ID => {:type => ::Thrift::Types::I32, :name => 'state_id'},
116
+ COUNT => {:type => ::Thrift::Types::I32, :name => 'count'},
117
+ UPDATED_AT => {:type => ::Thrift::Types::I32, :name => 'updated_at'}
118
+ }
119
+
120
+ def struct_fields; FIELDS; end
121
+
122
+ def validate
123
+ end
124
+
125
+ ::Thrift::Struct.generate_accessors self
126
+ end
127
+
106
128
  class Edge
107
129
  include ::Thrift::Struct, ::Thrift::Struct_Union
108
130
  SOURCE_ID = 1
@@ -2,13 +2,9 @@ module Flock
2
2
  module Mixins
3
3
  module Sizeable
4
4
  def any?(&block)
5
- if !block_given?
6
- size > 0
7
- else
8
- map(&block).any?
9
- end
5
+ block_given? ? map(&block).any? : size > 0
10
6
  end
11
-
7
+
12
8
  def empty?
13
9
  size == 0
14
10
  end
@@ -7,24 +7,30 @@ module Flock
7
7
  Edges::ExecuteOperationType::Archive => :archive,
8
8
  Edges::ExecuteOperationType::Negate => :negate
9
9
  }
10
+ OP_COLOR_MAP = {:add => 0, :remove => 1, :archive => 2, :negate => 3}
10
11
 
11
12
  attr_accessor :timeout, :fixtures
12
13
 
13
14
  def clear
14
- @forward_edges = nil
15
- @backward_edges = nil
15
+ @graphs = nil
16
+ end
17
+
18
+ def load_yml(file)
19
+ @data_for_fixture ||= Hash.new do |h, k|
20
+ h[k] = YAML::load(ERB.new(File.open(k, 'r').read).result(binding)).sort
21
+ end
22
+ @data_for_fixture[file]
16
23
  end
17
24
 
18
25
  def load(fixtures = nil)
19
26
  fixtures ||= self.fixtures or raise "No flock fixtures specified. either pass fixtures to load, or set Flock::MockService.fixtures."
20
-
21
27
  clear
22
28
 
23
29
  fixtures.each do |fixture|
24
30
  file, graph, source, dest = fixture.values_at(:file, :graph, :source, :destination)
25
31
 
26
- YAML::load(ERB.new(File.open(file, 'r').read).result(binding)).sort.each do |key, row|
27
- add(row[source], graph, row[dest])
32
+ load_yml(file).each do |key, row|
33
+ color_node(row[source], graph, row[dest], OP_COLOR_MAP[:add])
28
34
  end
29
35
  end
30
36
  end
@@ -37,21 +43,52 @@ module Flock
37
43
  operations = operations.operations
38
44
  operations.each do |operation|
39
45
  term = operation.term
46
+ position = operation.position
40
47
  graph, source = term.graph_id, term.source_id
41
- dest = term.destination_ids && term.destination_ids.unpack('Q*')
48
+ destinations = term.destination_ids && term.destination_ids.unpack('Q*')
49
+ dest_state = OP_COLOR_MAP[EXEC_OPS[operation.operation_type]]
42
50
 
43
- source, dest = dest, source unless term.is_forward
44
-
45
- self.send(EXEC_OPS[operation.operation_type], source, graph, dest)
51
+ source, destinations = destinations, source unless term.is_forward
52
+ color_node(source, graph, destinations, dest_state, position)
46
53
  end
47
54
  end
48
55
 
49
56
  def select(select_operations, page)
50
- iterate(select_query(select_operations), page)
57
+ data, next_cursor, prev_cursor = paginate(select_query(select_operations), page)
58
+
59
+ result = Flock::Results.new
60
+ result.ids = data.pack("Q*")
61
+ result.next_cursor = next_cursor
62
+ result.prev_cursor = prev_cursor
63
+ result
64
+ end
65
+
66
+ def select2(queries)
67
+ queries.map do |query|
68
+ select(query.operations, query.page)
69
+ end
70
+ end
71
+
72
+ def select_edges(queries)
73
+ queries.map do |query|
74
+ edges, next_cursor, prev_cursor = paginate(simple_query(query.term), query.page)
75
+ result = Edges::EdgeResults.new
76
+ result.edges = if query.term.is_forward
77
+ edges.map(&:dup)
78
+ else
79
+ edges.map(&:dup).map do |edge|
80
+ edge.source_id, edge.destination_id = edge.destination_id, edge.source_id
81
+ edge
82
+ end
83
+ end
84
+ result.next_cursor = next_cursor
85
+ result.prev_cursor = prev_cursor
86
+ result
87
+ end
51
88
  end
52
89
 
53
90
  def contains(source, graph, dest)
54
- forward_edges[graph][Edges::EdgeState::Positive][source].include?(dest)
91
+ graphs(graph).contains?(source, dest, Edges::EdgeState::Positive)
55
92
  end
56
93
 
57
94
  def count(select_operations)
@@ -66,10 +103,13 @@ module Flock
66
103
  case select_operation.operation_type
67
104
  when Edges::SelectOperationType::SimpleQuery
68
105
  term = select_operation.term
69
- source = term.is_forward ? forward_edges : backward_edges
70
- states = term.state_ids || [Edges::EdgeState::Positive]
71
- data = source[term.graph_id].inject([]) {|r, (s, e)| states.include?(s) ? r.concat(e[term.source_id]) : r }.uniq
72
- stack.push(term.destination_ids ? (term.destination_ids.unpack('Q*') & data) : data)
106
+ matching_edges = simple_query(term)
107
+ ids = if term.is_forward
108
+ matching_edges.map(&:destination_id)
109
+ else
110
+ matching_edges.map(&:source_id)
111
+ end
112
+ stack.push(ids)
73
113
  when Edges::SelectOperationType::Intersection
74
114
  stack.push(stack.pop & stack.pop)
75
115
  when Edges::SelectOperationType::Union
@@ -83,7 +123,18 @@ module Flock
83
123
  stack.pop
84
124
  end
85
125
 
86
- def iterate(data, page)
126
+ def simple_query(term)
127
+ destination_ids = term.destination_ids && term.destination_ids.unpack("Q*")
128
+ states = term.state_ids || [Edges::EdgeState::Positive]
129
+ graph = graphs(term.graph_id)
130
+ if term.is_forward
131
+ graph.select_edges(term.source_id, destination_ids, states)
132
+ else
133
+ graph.select_edges(destination_ids, term.source_id, states)
134
+ end
135
+ end
136
+
137
+ def paginate(data, page)
87
138
  return empty_result if page.cursor == Flock::CursorEnd
88
139
 
89
140
  start =
@@ -95,13 +146,8 @@ module Flock
95
146
  rv = data.slice(start, page.count)
96
147
  next_cursor = (start + page.count >= data.size) ? Flock::CursorEnd : start + page.count
97
148
  prev_cursor = (start <= 0) ? Flock::CursorEnd : -start
98
-
99
- result = Flock::Results.new
100
- result.ids = Array(rv).pack("Q*")
101
- result.next_cursor = next_cursor
102
- result.prev_cursor = prev_cursor
103
- result
104
- end
149
+ [Array(rv), next_cursor, prev_cursor]
150
+ end
105
151
 
106
152
  def empty_result
107
153
  @empty_result ||=
@@ -114,81 +160,100 @@ module Flock
114
160
  end
115
161
  end
116
162
 
117
-
118
- [:forward_edges, :backward_edges].each do |name|
119
- class_eval("def #{name}; @#{name} ||= new_edges_hash end")
163
+ def graphs(graph_id)
164
+ (@graphs ||= Hash.new do |h, k|
165
+ h[k] = Graph.new
166
+ end)[graph_id]
120
167
  end
121
168
 
122
- def new_edges_hash
123
- Hash.new { |h,k| h[k] = Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } } }
124
- end
125
169
 
126
- def add_row(store, source, dest)
127
- (store[source] << dest).uniq!
128
- end
129
-
130
- def add_edge(source, graph, dest, state)
131
- add_row(forward_edges[graph][state], source, dest)
132
- add_row(backward_edges[graph][state], dest, source)
133
- [source, graph, dest]
134
- end
170
+ def color_node(source, graph_id, dest, dest_state, position = nil)
171
+ raise ArgumentError unless Edges::EdgeState::VALUE_MAP.keys.include? dest_state
172
+ raise ArgumentError if source.nil? && dest.nil?
173
+ position ||= Time.now.to_i
135
174
 
136
- def remove_row(store, source, dest)
137
- store[source].delete(dest).tap do
138
- store.delete(source) if store[source].empty?
175
+ if source.nil? || dest.nil?
176
+ existing_edges = graphs(graph_id).select_edges(source, dest)
177
+ existing_edges.each do |existing_edge|
178
+ graphs(graph_id).add_edge(dest_state, existing_edge.source_id, existing_edge.destination_id, Time.now, position)
179
+ end
180
+ else
181
+ Array(source).each do |s|
182
+ Array(dest).each do |d|
183
+ graphs(graph_id).add_edge(dest_state, s, d, Time.now, position)
184
+ end
185
+ end
139
186
  end
140
187
  end
141
188
 
142
- def remove_edge(source, graph, dest, state)
143
- forward = remove_row(forward_edges[graph][state], source, dest)
144
- backward = remove_row(backward_edges[graph][state], dest, source)
189
+ end
145
190
 
146
- [source, graph, dest] if forward and backward
191
+ class Graph
192
+ def initialize
193
+ @by_pair, @by_source, @by_destination = {}, Hash.new { |h, k| h[k] = [] }, Hash.new { |h, k| h[k] = [] }
147
194
  end
148
195
 
149
- def remove_node(source, graph, dest, state)
150
- raise unless source or dest
151
-
152
- sources, dests = Array(source), Array(dest)
153
-
154
- sources = dests.map{|dest| backward_edges[graph][state][dest] }.inject([]) {|a,b| a.concat b } if sources.empty?
155
- dests = sources.map{|source| forward_edges[graph][state][source] }.inject([]) {|a,b| a.concat b } if dests.empty?
156
-
157
- [].tap do |deleted|
158
- sources.each {|s| dests.each {|d| deleted << remove_edge(s, graph, d, state) } }
159
- end.compact
196
+ def add_edge(state, source, dest, time, pos)
197
+ if existing_edge = @by_pair[[source, dest]]
198
+ if ![Edges::EdgeState::Positive, Edges::EdgeState::Archived].include?(existing_edge.state_id) && state == Edges::EdgeState::Positive
199
+ existing_edge.position = pos
200
+ end
201
+ existing_edge.state_id = state
202
+ existing_edge.updated_at = time.to_i
203
+ else
204
+ edge = make_edge(state, source, dest, time, pos)
205
+ @by_pair[[source, dest]] = edge
206
+ @by_source[source] << edge
207
+ @by_destination[dest] << edge
208
+ end
160
209
  end
161
210
 
162
- def color_node(source, graph, dest, dest_state)
163
- raise ArgumentError unless Edges::EdgeState::VALUE_MAP.keys.include? dest_state
164
-
165
- source_states = (Edges::EdgeState::VALUE_MAP.keys - [dest_state])
211
+ def select_edges(source_ids, destination_ids, states = [])
212
+ source_ids, destination_ids, states = Array(source_ids), Array(destination_ids), Array(states)
213
+ result = []
214
+ if source_ids.empty?
215
+ destination_ids.each do |destination_id|
216
+ @by_destination[destination_id].each do |edge|
217
+ next unless states.empty? || states.include?(edge.state_id)
166
218
 
167
- if source.nil? or dest.nil?
168
- source_states.each do |source_state|
169
- remove_node(source, graph, dest, source_state).tap do |deleted|
170
- deleted.each {|source, graph, dest| add_edge(source, graph, dest, dest_state) }
219
+ result << edge
171
220
  end
172
221
  end
222
+ elsif destination_ids.empty?
223
+ source_ids.each do |source_id|
224
+ @by_source[source_id].each do |edge|
225
+ next unless states.empty? || states.include?(edge.state_id)
173
226
 
227
+ result << edge
228
+ end
229
+ end
174
230
  else
175
- sources, dests = Array(source), Array(dest)
231
+ source_ids.each do |source_id|
232
+ destination_ids.each do |destination_id|
233
+ next unless existing_edge = @by_pair[[source_id, destination_id]]
234
+ next unless states.empty? || states.include?(existing_edge.state_id)
176
235
 
177
- sources.each do |s|
178
- dests.each do |d|
179
- add_edge(s, graph, d, dest_state)
180
- source_states.each { |state| remove_edge(s, graph, d, state) }
236
+ result << existing_edge
181
237
  end
182
238
  end
183
239
  end
240
+ result
184
241
  end
185
242
 
186
- # must map manually since execute operations do not line up with
187
- # their actual colors
188
- op_color_map = {:add => 0, :remove => 1, :archive => 2, :negate => 3}
243
+ def contains?(source, dest, state)
244
+ select_edges(source, dest, state).any?
245
+ end
189
246
 
190
- op_color_map.each do |op, color|
191
- class_eval "def #{op}(s, g, d); color_node(s, g, d, #{color}) end", __FILE__, __LINE__
247
+ private
248
+ def make_edge(state, source, dest, time, pos)
249
+ Edges::Edge.new.tap do |edge|
250
+ edge.source_id = source
251
+ edge.destination_id = dest
252
+ edge.updated_at = time.to_i
253
+ edge.position = pos
254
+ edge.count = 1
255
+ edge.state_id = state
256
+ end
192
257
  end
193
258
  end
194
259
  end
@@ -8,7 +8,7 @@ module Flock
8
8
  op = Edges::ExecuteOperation.new
9
9
  op.operation_type = @operation_type
10
10
  op.position = @position if @position
11
- op.term = QueryTerm.new(@query).to_thrift
11
+ op.term = @query.to_thrift
12
12
  op
13
13
  end
14
14
  end
@@ -6,7 +6,12 @@ module Flock
6
6
 
7
7
  Flock::Edges::ExecuteOperationType::VALUE_MAP.each do |op_id, op|
8
8
  op = op.downcase
9
- class_eval "def #{op}(s, g, d, p = nil); @operations << ExecuteOperation.new(#{op_id}, [s, g, d], p); self end", __FILE__, __LINE__
9
+ class_eval <<-EOT, __FILE__, __LINE__ + 1
10
+ def #{op}(s, g, d, p = nil)
11
+ @operations << ExecuteOperation.new(#{op_id}, Flock::QueryTerm.new([s, g, d]), p)
12
+ self
13
+ end
14
+ EOT
10
15
  end
11
16
 
12
17
  def apply
@@ -1,20 +1,27 @@
1
1
  module Flock
2
2
  class QueryTerm
3
+ # symbol => state_id map
4
+ STATES = Flock::Edges::EdgeState::VALUE_MAP.inject({}) do |states, (id, name)|
5
+ states.update name.downcase.to_sym => id
6
+ end.freeze
7
+
3
8
  attr_accessor :source, :graph, :destination, :states
4
9
 
5
- def initialize(query)
6
- case query.size
7
- when 3, 4
8
- @source, @graph, @destination, @states = *query
9
- else
10
- raise ArgumentError
11
- end
10
+ def initialize(query, graphs = nil)
11
+ raise ArgumentError if query.size < 3
12
+ @graphs = graphs
13
+
14
+ @source, @graph, @destination, @states = *query_args(query)
12
15
  end
13
16
 
14
17
  def forward?
15
18
  @source.is_a? Numeric
16
19
  end
17
20
 
21
+ def unapply
22
+ [@source, @graph, @destination, @states]
23
+ end
24
+
18
25
  def to_thrift
19
26
  term = Edges::QueryTerm.new
20
27
  term.graph_id = @graph
@@ -33,5 +40,42 @@ module Flock
33
40
 
34
41
  term
35
42
  end
43
+
44
+ def ==(other)
45
+ self.source == other.source &&
46
+ self.graph == other.graph &&
47
+ self.destination == other.destination &&
48
+ self.states == other.states
49
+ end
50
+
51
+ private
52
+
53
+ def lookup_graph(key)
54
+ if @graphs.nil? or key.is_a? Integer
55
+ key
56
+ else
57
+ @graphs[key] or raise UnknownGraphError.new(key)
58
+ end
59
+ end
60
+
61
+ def lookup_states(states)
62
+ states = states.flatten.compact.map do |s|
63
+ if s.is_a? Integer
64
+ s
65
+ else
66
+ STATES[s] or raise UnknownStateError.new(s)
67
+ end
68
+ end
69
+ end
70
+
71
+ def query_args(args)
72
+ source, graph, destination, *states = *((args.length == 1) ? args.first : args)
73
+ [node_arg(source), lookup_graph(graph), node_arg(destination), lookup_states(states)]
74
+ end
75
+
76
+ def node_arg(node)
77
+ return node.map {|n| n.to_i if n } if node.respond_to? :map
78
+ node.to_i if node
79
+ end
36
80
  end
37
81
  end
@@ -1,4 +1,3 @@
1
-
2
1
  module Flock
3
2
  class SelectOperation
4
3
  [:each, :paginate, :to_ary, :size, :first].each do |method|