Completed
Push — master ( ea113f...7c511e )
by Michael
03:41
created

StatementGenerator.random_where_clause()   A

Complexity

Conditions 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 2
rs 9.4285
1 1
require 'pickup'
0 ignored issues
show
coding-style introduced by
Missing frozen string literal comment.
Loading history...
2
3 1
module NoSE
4 1
  module Random
5
    # A simple representation of a random ER diagram
6 1
    class Network
7 1
      attr_reader :entities
8
9 1
      def initialize(params = {})
10 6
        @nodes_nb = params.fetch :nodes_nb, 10
11 6
        @field_count = RandomGaussian.new params.fetch(:num_fields, 3), 1
12 66
        @neighbours = Array.new(@nodes_nb) { Set.new }
13
      end
14
15
      # :nocov:
16 1
      def inspect
17
        @nodes.map do |node|
18
          @entities[node].inspect
19
        end.join "\n"
20
      end
21
      # :nocov:
22
23 1
      protected
24
25
      # Create a random entity to use in the model
26
      # @return [Entity]
27 1
      def create_entity(node)
28 60
        num_entities = RandomGaussian.new 10_000, 100
29 60
        entity = Entity.new('E' + random_name(node)) * num_entities.rand
30 60
        pick_fields entity
31
32 60
        entity
33
      end
34
35
      # Probabilities of selecting various field types
36 1
      FIELD_TYPES = {
37
        Fields::IntegerField => 45,
38
        Fields::StringField =>  35,
39
        Fields::DateField =>    10,
40
        Fields::FloatField =>   10
41
      }.freeze
42
43
      # Select random fields for an entity
44
      # @return [void]
45 1
      def pick_fields(entity)
46 60
        entity << Fields::IDField.new(entity.name + 'ID')
47 60
        0.upto(@field_count.rand).each do |field_index|
48 210
          entity << random_field(field_index)
49
        end
50
      end
51
52
      # Generate a random field to add to an entity
53
      # @return [Fields::Field]
54 1
      def random_field(field_index)
55 210
        Pickup.new(FIELD_TYPES).pick.send(:new, 'F' + random_name(field_index))
56
      end
57
58
      # Add foreign key relationships for neighbouring nodes
59
      # @return [void]
60 1
      def add_foreign_keys
0 ignored issues
show
Coding Style introduced by
The Assignment, Branch, Condition size for add_foreign_keys is considered too high. [23.43/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
Coding Style introduced by
This method is 24 lines long. Your coding style permits a maximum length of 20.
Loading history...
61 6
        @neighbours.each_with_index do |other_nodes, node|
62 60
          other_nodes.each do |other_node|
63 102
            @neighbours[other_node].delete node
64
65 102
            if rand > 0.5
66 55
              from_node = node
67 55
              to_node = other_node
68
            else
69 47
              from_node = other_node
70 47
              to_node = node
71
            end
72
73 102
            from_field = Fields::ForeignKeyField.new(
74
              'FK' + @entities[to_node].name + 'ID',
75
              @entities[to_node]
76
            )
77 102
            to_field = Fields::ForeignKeyField.new(
78
              'FK' + @entities[from_node].name + 'ID',
79
              @entities[from_node]
80
            )
81
82 102
            from_field.reverse = to_field
83 102
            to_field.reverse = from_field
84
85 102
            @entities[from_node] << from_field
86 102
            @entities[to_node] << to_field
87
          end
88
        end
89
      end
90
91
      # Add a new link between two nodes
92
      # @return [void]
93 1
      def add_link(node, other_node)
94 102
        @neighbours[node] << other_node
95 102
        @neighbours[other_node] << node
96
      end
97
98
      # Remove a link between two nodes
99
      # @return [void]
100 1
      def remove_link(node, other_node)
101
        @neighbours[node].delete other_node
102
        @neighbours[other_node].delete node
103
      end
104
105
      # Find a new neighbour for a node
106 1
      def new_neighbour(node, neighbour)
107
        unlinkable_nodes = [node, neighbour] + @neighbours[node].to_a
108
        (@nodes.to_a - unlinkable_nodes).sample
109
      end
110
111
      # Random names of variables combined to create random names
112 1
      VARIABLE_NAMES = %w(Foo Bar Baz Quux Corge Grault
113
                          Garply Waldo Fred Plugh).freeze
114
115
      # Generate a random name for an attribute
116
      # @return [String]
117 1
      def random_name(index)
118 540
        index.to_s.chars.map(&:to_i).map { |digit| VARIABLE_NAMES[digit] }.join
119
      end
120
    end
121
122
    # Generates random queries over entities in a given model
123 1
    class StatementGenerator
124 1
      def initialize(model)
125 5
        @model = model
126
      end
127
128
      # Generate a new random insertion to entities in the model
129
      # @return [Insert]
130 1
      def random_insert(connect = true)
131 1
        entity = @model.entities.values.sample
132 1
        settings = entity.fields.each_value.map do |field|
133 5
          "#{field.name}=?"
134
        end.join ', '
135 1
        insert = "INSERT INTO #{entity.name} SET #{settings} "
136
137
        # Optionally add connections to other entities
138 1
        insert += random_connection(entity) if connect
139
140 1
        Statement.parse insert, @model
141
      end
142
143
      # Generate a random connection for an Insert
144 1
      def random_connection(entity)
145 1
        connections = entity.foreign_keys.values.sample(2)
146
        'AND CONNECT TO ' + connections.map do |connection|
147 2
          "#{connection.name}(?)"
148 1
        end.join(', ')
149
      end
150
151
      # Generate a new random update of entities in the model
152
      # @return [Update]
153 1
      def random_update(path_length = 1, updated_fields = 2,
154
                        condition_count = 1)
155 1
        path = random_path(path_length)
156 1
        settings = random_settings path, updated_fields
157 1
        from = [path.first.parent.name] + path.entries[1..-1].map(&:name)
158 1
        update = "UPDATE #{from.first} FROM #{from.join '.'} " \
159
                 "SET #{settings} " +
160
                 random_where_clause(path, condition_count)
161
162 1
        Statement.parse update, @model
163
      end
164
165
      # Get random settings for an update
166
      # @return [String]
167 1
      def random_settings(path, updated_fields)
168
        # Don't update key fields
169 1
        update_fields = path.entities.first.fields.values
170 5
        update_fields.reject! { |field| field.is_a? Fields::IDField }
171
172 1
        update_fields.sample(updated_fields).map do |field|
173 2
          "#{field.name}=?"
174
        end.join ', '
175
      end
176
177
      # Generate a new random deletion of entities in the model
178
      # @return [Delete]
179 1
      def random_delete
180 1
        path = random_path(1)
181
182 1
        from = [path.first.parent.name] + path.entries[1..-1].map(&:name)
183 1
        delete = "DELETE #{from.first} FROM #{from.join '.'} " +
184
                 random_where_clause(path, 1)
185
186 1
        Statement.parse delete, @model
187
      end
188
189
      # Generate a new random query from entities in the model
190
      # @return [Query]
191 1
      def random_query(path_length = 3, selected_fields = 2,
0 ignored issues
show
Coding Style introduced by
The Assignment, Branch, Condition size for random_query is considered too high. [31.97/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
Coding Style introduced by
This method is 27 lines long. Your coding style permits a maximum length of 20.
Loading history...
192
                       condition_count = 2, order = false)
193 1
        path = random_path path_length
194 1
        graph = QueryGraph::Graph.from_path path
195
196 1
        conditions = [
197
          Condition.new(path.entities.first.fields.values.sample, :'=', nil)
198
        ]
199 1
        condition_count -= 1
200
201 1
        new_fields = random_where_conditions(path, condition_count,
202
                                             conditions.map(&:field).to_set)
203 1
        conditions += new_fields.map do |field|
204 1
          Condition.new(field, :'>', nil)
205
        end
206
207 1
        conditions = Hash[conditions.map do |condition|
208 2
          [condition.field.id, condition]
209
        end]
210
211 1
        params = {
212
          select: random_select(path, selected_fields),
213
          model: @model,
214
          graph: graph,
215
          key_path: graph.longest_path,
216
          entity: graph.longest_path.first.parent,
217
          conditions: conditions,
218
          order: order ? [graph.entities.to_a.sample.fields.values.sample] : []
219
        }
220
221 1
        query = Query.new params, nil
222 1
        query.hash
223
224 1
        query
225
      end
226
227
      # Get random fields to select for a Query
228
      # @return [Set<Fields::Field>]
229 1
      def random_select(path, selected_fields)
230 1
        fields = Set.new
231 1
        while fields.length < selected_fields
232 2
          fields.add path.entities.sample.fields.values.sample
233
        end
234
235 1
        fields
236
      end
237
238
      # Produce a random statement according to a given set of weights
239
      # @return [Statement]
240 1
      def random_statement(weights = { query: 80, insert: 10, update: 5,
241
                                       delete: 5 })
242
        pick = Pickup.new(weights)
243
        type = pick.pick
244
        send(('random_' + type.to_s).to_sym)
245
      end
246
247
      # Return a random path through the entity graph
248
      # @return [KeyPath]
249 1
      def random_path(max_length)
0 ignored issues
show
Coding Style introduced by
The Assignment, Branch, Condition size for random_path is considered too high. [23.43/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
250
        # Pick the start of path weighted based on
251
        # the number of deges from each entity
252 3
        pick = Pickup.new(Hash[@model.entities.each_value.map do |entity|
253 9
          [entity, entity.foreign_keys.length]
254
        end])
255 3
        path = [pick.pick.id_field]
256
257 3
        while path.length < max_length
258
          # Find a list of keys to entities we have not seen before
259 2
          last_entity = path.last.entity
260 2
          keys = last_entity.foreign_keys.values
261 7
          keys.reject! { |key| path.map(&:entity).include? key.entity }
262 2
          break if keys.empty?
263
264
          # Add a random new key to the path
265 1
          path << keys.sample
266
        end
267
268 3
        KeyPath.new path
269
      end
270
271
      # Produce a random query graph over the entity graph
272 1
      def random_graph(max_nodes)
0 ignored issues
show
Coding Style introduced by
The Assignment, Branch, Condition size for random_graph is considered too high. [31.54/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
273 1
        graph = QueryGraph::Graph.new
274 1
        last_node = graph.add_node @model.entities.values.sample
275 1
        while graph.size < max_nodes
276
          # Get the possible foreign keys to use
277 2
          keys = last_node.entity.foreign_keys.values
278 7
          keys.reject! { |key| graph.nodes.map(&:entity).include? key.entity }
279 2
          break if keys.empty?
280
281
          # Pick a random foreign key to traverse
282 2
          next_key = keys.sample
283 2
          graph.add_edge last_node, next_key.entity, next_key
284
285
          # Select a new node to start from, making sure we pick one
286
          # that still has valid outgoing edges
287 2
          last_node = graph.nodes.reject do |node|
288
            (node.entity.foreign_keys.each_value.map(&:entity) -
289 5
             graph.nodes.map(&:entity)).empty?
290
          end.sample
291 2
          break if last_node.nil?
292
        end
293
294 1
        graph
295
      end
296
297 1
      private
298
299
      # Produce a random where clause using fields along a given path
300
      # @return [String]
301 1
      def random_where_clause(path, count = 2)
302
        # Ensure we have at least one condition at the beginning of the path
303 2
        conditions = [path.entities.first.fields.values.sample]
304 2
        conditions += random_where_conditions path, count - 1
305
306 2
        return '' if conditions.empty?
307 2
        "WHERE #{conditions.map do |field|
308 2
          "#{path.find_field_parent(field).name}.#{field.name} = ?"
309
        end.join ' AND '}"
310
      end
311
312
      # Produce a random set of fields for a where clause
313
      # @return [Array<Fields::Field>]
314 1
      def random_where_conditions(path, count, exclude = Set.new)
315
        1.upto(count).map do
316 1
          field = path.entities.sample.fields.values.sample
317 1
          next nil if field.name == '**' || exclude.include?(field)
318
319 1
          field
320 3
        end.compact
321
      end
322
323
      # Get the name to be used in the query for a condition field
324
      # @return [String]
325 1
      def condition_field_name(field, path)
326
        field_path = path.first.name
327
        path_end = path.index(field.parent)
328
        last_entity = path.first
329
        path[1..path_end].each do |entity|
330
          fk = last_entity.foreign_keys.values.find do |key|
331
            key.entity == entity
332
          end
333
          field_path += '.' + fk.name
334
          last_entity = entity
335
        end
336
337
        field_path
338
      end
339
    end
340
  end
341
342
  # Generate random numbers according to a Guassian distribution
343 1
  class RandomGaussian
344 1
    def initialize(mean, stddev, integer = true, min = 1)
345 66
      @mean = mean
346 66
      @stddev = stddev
347 66
      @valid = false
348 66
      @next = 0
349 66
      @integer = integer
350 66
      @min = min
351
    end
352
353
    # Return the next valid random number
354
    # @return [Fixnum]
355 1
    def rand
356 120
      if @valid
357 30
        @valid = false
358 30
        clamp @next
359
      else
360 90
        @valid = true
361 90
        x, y = self.class.gaussian(@mean, @stddev)
362 90
        @next = y
363 90
        clamp x
364
      end
365
    end
366
367 1
    private
368
369
    # Clamp the value to the given minimum
370 1
    def clamp(value)
371 120
      value = value.to_i if @integer
372 120
      [@min, value].max unless @min.nil?
373
    end
374
375
    # Return a random number for the given distribution
376
    # @return [Array<Fixnum>]
377 1
    def self.gaussian(mean, stddev)
0 ignored issues
show
Bug introduced by
private (on line 367) does not make singleton methods private. Use private_class_method or private inside a class << self block instead.
Loading history...
378 90
      theta = 2 * Math::PI * rand
379 90
      rho = Math.sqrt(-2 * Math.log(1 - rand))
380 90
      scale = stddev * rho
381 90
      x = mean + scale * Math.cos(theta)
382 90
      y = mean + scale * Math.sin(theta)
383 90
      [x, y]
384
    end
385
  end
386
end
387
388 1
require_relative 'random/barbasi_albert'
389
require_relative 'random/watts_strogatz'
390