Completed
Push — master ( b4ef5f...98b872 )
by Michael
03:16
created

StatementGenerator.condition_field_name()   A

Complexity

Conditions 1

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.729

Importance

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