Completed
Push — master ( 3f4b33...e37d52 )
by Michael
03:01
created

RandomGaussian.gaussian()   A

Complexity

Conditions 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
dl 0
loc 8
ccs 7
cts 7
cp 1
crap 1
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 195
          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 195
        type_rand = rand
56
        FIELD_TYPES.find do |_, threshold|
57 216
          type_rand -= threshold
58 216
          type_rand <= threshold
59 195
        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 69
            @neighbours[other_node].delete node
68
69 69
            if rand > 0.5
70 39
              from_node = node
71 39
              to_node = other_node
72
            else
73 30
              from_node = other_node
74 30
              to_node = node
75
            end
76
77 69
            from_field = Fields::ForeignKeyField.new(
78
              'FK' + @entities[to_node].name + 'ID',
79
              @entities[to_node]
80
            )
81 69
            to_field = Fields::ForeignKeyField.new(
82
              'FK' + @entities[from_node].name + 'ID',
83
              @entities[from_node]
84
            )
85
86 69
            from_field.reverse = to_field
87 69
            to_field.reverse = from_field
88
89 69
            @entities[from_node] << from_field
90 69
            @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 510
        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, condition_count = 1)
0 ignored issues
show
Coding Style introduced by
This line is 81 characters long. Your coding style permits a maximum length of 80
Loading history...
158 1
        path = random_path(path_length)
159 1
        settings = random_settings path, updated_fields
160 1
        from = [path.first.parent.name] + path.entries[1..-1].map(&:name)
161 1
        update = "UPDATE #{from.first} FROM #{from.join '.'} SET #{settings} " +
162
                 random_where_clause(path, condition_count)
163
164 1
        Statement.parse update, @model
165
      end
166
167
      # Get random settings for an update
168
      # @return [String]
169 1
      def random_settings(path, updated_fields)
170
        # Don't update key fields
171 1
        update_fields = path.entities.first.fields.values
172 6
        update_fields.reject! { |field| field.is_a? Fields::IDField }
173
174 1
        update_fields.sample(updated_fields).map do |field|
175 2
          "#{field.name}=?"
176
        end.join ', '
177
      end
178
179
      # Generate a new random deletion of entities in the model
180
      # @return [Delete]
181 1
      def random_delete
182 1
        path = random_path(1)
183
184 1
        from = [path.first.parent.name] + path.entries[1..-1].map(&:name)
185 1
        delete = "DELETE #{from.first} FROM #{from.join '.'} " +
186
                 random_where_clause(path, 1)
187
188 1
        Statement.parse delete, @model
189
      end
190
191
      # Generate a new random query from entities in the model
192
      # @return [Query]
193 1
      def random_query(path_length = 3, selected_fields = 2, condition_count = 2,
0 ignored issues
show
Coding Style introduced by
The Assignment, Branch, Condition size for random_query is considered too high. [31.64/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
Coding Style introduced by
This method is 26 lines long. Your coding style permits a maximum length of 20.
Loading history...
Coding Style introduced by
This line is 81 characters long. Your coding style permits a maximum length of 80
Loading history...
194
                       order = false)
195 1
        path = random_path path_length
196 1
        graph = QueryGraph::Graph.from_path path
197
198 1
        conditions = [
199
          Condition.new(path.entities.first.fields.values.sample, :'=', nil)
200
        ]
201 1
        condition_count -= 1
202 1
        conditions += random_where_conditions(path, condition_count,
203
                                              conditions.map(&:field).to_set).map do |field|
0 ignored issues
show
Coding Style introduced by
This line is 92 characters long. Your coding style permits a maximum length of 80
Loading history...
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(1)
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)
250 3
        path = [@model.entities.values.sample.id_field]
251 3
        while path.length < max_length
252
          # Find a list of keys to entities we have not seen before
253 2
          last_entity = path.last.entity
254 2
          keys = last_entity.foreign_keys.values
255 7
          keys.reject! { |key| path.map(&:entity).include? key.entity }
256 2
          break if keys.empty?
257
258
          # Add a random new key to the path
259 2
          path << keys.sample
260
        end
261
262 3
        KeyPath.new path
263
      end
264
265
      # Produce a random query graph over the entity graph
266 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...
267 1
        graph = QueryGraph::Graph.new
268 1
        last_node = graph.add_node @model.entities.values.sample
269 1
        while graph.size < max_nodes
270
          # Get the possible foreign keys to use
271 2
          keys = last_node.entity.foreign_keys.values
272 7
          keys.reject! { |key| graph.nodes.map(&:entity).include? key.entity }
273 2
          break if keys.empty?
274
275
          # Pick a random foreign key to traverse
276 2
          next_key = keys.sample
277 2
          graph.add_edge last_node, next_key.entity, next_key
278
279
          # Select a new node to start from, making sure we pick one
280
          # that still has valid outgoing edges
281 2
          last_node = graph.nodes.reject do |node|
282
            (node.entity.foreign_keys.each_value.map(&:entity) -
283 5
             graph.nodes.map(&:entity)).empty?
284
          end.sample
285 2
          break if last_node.nil?
286
        end
287
288 1
        graph
289
      end
290
291 1
      private
292
293
      # Produce a random where clause using fields along a given path
294
      # @return [String]
295 1
      def random_where_clause(path, count = 2)
296
        # Ensure we have at least one condition at the beginning of the path
297 2
        conditions = [path.entities.first.fields.values.sample]
298 2
        conditions += random_where_conditions path, count - 1
299
300 2
        return '' if conditions.empty?
301 2
        "WHERE #{conditions.map do |field|
302 2
          "#{path.find_field_parent(field).name}.#{field.name} = ?"
303
        end.join ' AND '}"
304
      end
305
306
      # Produce a random set of fields for a where clause
307
      # @return [Array<Fields::Field>]
308 1
      def random_where_conditions(path, count, exclude = Set.new)
309
        1.upto(count).map do
310 1
          field = path.entities.sample.fields.values.sample
311 1
          next nil if field.name == '**' || exclude.include?(field)
312
313 1
          field
314 3
        end.compact
315
      end
316
317
      # Get the name to be used in the query for a condition field
318
      # @return [String]
319 1
      def condition_field_name(field, path)
320
        field_path = path.first.name
321
        path_end = path.index(field.parent)
322
        last_entity = path.first
323
        path[1..path_end].each do |entity|
324
          fk = last_entity.foreign_keys.values.find do |key|
325
            key.entity == entity
326
          end
327
          field_path += '.' + fk.name
328
          last_entity = entity
329
        end
330
331
        field_path
332
      end
333
    end
334
  end
335
336
  # Generate random numbers according to a Guassian distribution
337 1
  class RandomGaussian
338 1
    def initialize(mean, stddev, integer = true, min = 1)
339 66
      @mean = mean
340 66
      @stddev = stddev
341 66
      @valid = false
342 66
      @next = 0
343 66
      @integer = integer
344 66
      @min = min
345
    end
346
347
    # Return the next valid random number
348
    # @return [Fixnum]
349 1
    def rand
350 120
      if @valid
351 30
        @valid = false
352 30
        clamp @next
353
      else
354 90
        @valid = true
355 90
        x, y = self.class.gaussian(@mean, @stddev)
356 90
        @next = y
357 90
        clamp x
358
      end
359
    end
360
361 1
    private
362
363
    # Clamp the value to the given minimum
364 1
    def clamp(value)
365 120
      value = value.to_i if @integer
366 120
      [@min, value].max unless @min.nil?
367
    end
368
369
    # Return a random number for the given distribution
370
    # @return [Array<Fixnum>]
371 1
    def self.gaussian(mean, stddev)
0 ignored issues
show
Bug introduced by
private (on line 361) does not make singleton methods private. Use private_class_method or private inside a class << self block instead.
Loading history...
372 90
      theta = 2 * Math::PI * rand
373 90
      rho = Math.sqrt(-2 * Math.log(1 - rand))
374 90
      scale = stddev * rho
375 90
      x = mean + scale * Math.cos(theta)
376 90
      y = mean + scale * Math.sin(theta)
377 90
      [x, y]
378
    end
379
  end
380
end
381
382 1
require_relative 'random/barbasi_albert'
383
require_relative 'random/watts_strogatz'
384