michaelmior /
NoSE
| 1 | # frozen_string_literal: true |
||
| 2 | |||
| 3 | 1 | module NoSE |
|
| 4 | # A single condition in a where clause |
||
| 5 | 1 | class Condition |
|
| 6 | 1 | attr_reader :field, :is_range, :operator, :value |
|
| 7 | 1 | alias range? is_range |
|
| 8 | |||
| 9 | 1 | def initialize(field, operator, value) |
|
| 10 | 499 | @field = field |
|
| 11 | 499 | @operator = operator |
|
| 12 | 499 | @is_range = [:>, :>=, :<, :<=].include? operator |
|
|
0 ignored issues
–
show
coding-style
introduced
by
Loading history...
|
|||
| 13 | 499 | @value = value |
|
| 14 | |||
| 15 | # XXX: Not frozen by now to support modification during query execution |
||
| 16 | # freeze |
||
| 17 | end |
||
| 18 | |||
| 19 | 1 | def inspect |
|
| 20 | "#{@field.inspect} #{@operator} #{value}" |
||
| 21 | end |
||
| 22 | |||
| 23 | # Compare conditions equal by their field and operator |
||
| 24 | # @return [Boolean] |
||
| 25 | 1 | def ==(other) |
|
| 26 | 7 | @field == other.field && @operator == other.operator |
|
| 27 | end |
||
| 28 | 1 | alias eql? == |
|
| 29 | |||
| 30 | 1 | def hash |
|
| 31 | 408 | Zlib.crc32 [@field.id, @operator].to_s |
|
| 32 | end |
||
| 33 | |||
| 34 | # If the condition is on a foreign key, resolve |
||
| 35 | # it to the primary key of the related entity |
||
| 36 | # @return [Condition] |
||
| 37 | 1 | def resolve_foreign_key |
|
| 38 | 7 | return self unless field.is_a?(Fields::ForeignKeyField) |
|
| 39 | |||
| 40 | Condition.new @field.entity.id_field, @operator, @value |
||
| 41 | end |
||
| 42 | end |
||
| 43 | |||
| 44 | # Used to add a list of conditions to a {Statement} |
||
| 45 | 1 | module StatementConditions |
|
| 46 | 1 | attr_reader :conditions |
|
| 47 | |||
| 48 | # @return [void] |
||
| 49 | 1 | def populate_conditions(params) |
|
| 50 | 314 | @conditions = params[:conditions] |
|
| 51 | 314 | @eq_fields = conditions.each_value.reject(&:range?).map(&:field).to_set |
|
| 52 | 314 | @range_field = conditions.each_value.find(&:range?) |
|
| 53 | 314 | @range_field = @range_field.field unless @range_field.nil? |
|
| 54 | end |
||
| 55 | |||
| 56 | 1 | def self.included(base) |
|
| 57 | 4 | base.extend ClassMethods |
|
| 58 | end |
||
| 59 | |||
| 60 | # Add methods to the class for populating conditions |
||
| 61 | 1 | module ClassMethods |
|
| 62 | 1 | private |
|
| 63 | |||
| 64 | # Extract conditions from a parse tree |
||
| 65 | # @return [Hash] |
||
| 66 | 1 | def conditions_from_tree(tree, params) |
|
| 67 | 215 | conditions = tree[:where].nil? ? [] : tree[:where][:expression] |
|
| 68 | 480 | conditions = conditions.map { |c| build_condition c, tree, params } |
|
| 69 | |||
| 70 | 213 | params[:conditions] = Hash[conditions.map do |condition| |
|
| 71 | 263 | [condition.field.id, condition] |
|
| 72 | end] |
||
| 73 | end |
||
| 74 | |||
| 75 | # Construct a condition object from the parse tree |
||
| 76 | # @return [void] |
||
| 77 | 1 | def build_condition(condition, tree, params) |
|
| 78 | 265 | field = add_field_with_prefix tree[:path], condition[:field], params |
|
| 79 | 265 | Condition.new field, condition[:op].to_sym, |
|
| 80 | condition_value(condition, field) |
||
| 81 | end |
||
| 82 | |||
| 83 | # Get the value of a condition from the parse tree |
||
| 84 | # @return [Object] |
||
| 85 | 1 | def condition_value(condition, field) |
|
| 86 | 265 | value = condition[:value] |
|
| 87 | |||
| 88 | # Convert the value to the correct type |
||
| 89 | 265 | type = field.class.const_get 'TYPE' |
|
| 90 | value = field.class.value_from_string(value.to_s) \ |
||
| 91 | 265 | unless type.nil? || value.nil? |
|
| 92 | |||
| 93 | # Don't allow predicates on foreign keys |
||
| 94 | fail InvalidStatementException, 'Predicates cannot use foreign keys' \ |
||
| 95 | 264 | if field.is_a? Fields::ForeignKeyField |
|
| 96 | |||
| 97 | 263 | condition.delete :value |
|
| 98 | |||
| 99 | 263 | value |
|
| 100 | end |
||
| 101 | end |
||
| 102 | end |
||
| 103 | |||
| 104 | # A path from a primary key to a chain of foreign keys |
||
| 105 | 1 | class KeyPath |
|
| 106 | 1 | include Enumerable |
|
| 107 | |||
| 108 | 1 | extend Forwardable |
|
| 109 | 1 | def_delegators :@keys, :each, :inspect, :to_s, :length, :count, :last, |
|
| 110 | :empty? |
||
| 111 | |||
| 112 | 1 | def initialize(keys = []) |
|
| 113 | fail InvalidKeyPathException, 'first key must be an ID' \ |
||
| 114 | 9160 | unless keys.empty? || keys.first.instance_of?(Fields::IDField) |
|
| 115 | |||
| 116 | 9159 | keys_match = keys.each_cons(2).all? do |prev_key, key| |
|
| 117 | 11775 | key.parent == prev_key.entity |
|
| 118 | end |
||
| 119 | fail InvalidKeyPathException, 'keys must match along the path' \ |
||
| 120 | 9159 | unless keys_match |
|
| 121 | |||
| 122 | 9158 | @keys = keys |
|
| 123 | end |
||
| 124 | |||
| 125 | # Two key paths are equal if their underlying keys are equal or the reverse |
||
| 126 | # @return [Boolean] |
||
| 127 | 1 | def ==(other, check_reverse = true) |
|
|
0 ignored issues
–
show
|
|||
| 128 | 4 | @keys == other.instance_variable_get(:@keys) || |
|
| 129 | (check_reverse && reverse.send(:==, other.reverse, false)) |
||
| 130 | end |
||
| 131 | 1 | alias eql? == |
|
| 132 | |||
| 133 | # Check if this path starts with another path |
||
| 134 | # @return [Boolean] |
||
| 135 | 1 | def start_with?(other, check_reverse = true) |
|
|
0 ignored issues
–
show
|
|||
| 136 | other_keys = other.instance_variable_get(:@keys) |
||
| 137 | @keys[0..other_keys.length - 1] == other_keys || |
||
| 138 | (check_reverse && reverse.start_with?(other.reverse, false)) |
||
| 139 | end |
||
| 140 | |||
| 141 | # Check if a key is included in the path |
||
| 142 | # @return [Boolean] |
||
| 143 | 1 | def include?(key) |
|
| 144 | 292 | @keys.include?(key) || entities.any? { |e| e.id_field == key } |
|
| 145 | end |
||
| 146 | |||
| 147 | # Combine two key paths by gluing together the keys |
||
| 148 | # @return [KeyPath] |
||
| 149 | 1 | def +(other) |
|
| 150 | fail TypeError unless other.is_a? KeyPath |
||
|
0 ignored issues
–
show
|
|||
| 151 | other_keys = other.instance_variable_get(:@keys) |
||
| 152 | |||
| 153 | # Just copy if there's no combining necessary |
||
| 154 | return dup if other_keys.empty? |
||
| 155 | return other.dup if @keys.empty? |
||
| 156 | |||
| 157 | # Only allow combining if the entities match |
||
| 158 | fail ArgumentError unless other_keys.first.parent == entities.last |
||
| 159 | |||
| 160 | # Combine the two paths |
||
| 161 | KeyPath.new(@keys + other_keys[1..-1]) |
||
|
0 ignored issues
–
show
|
|||
| 162 | end |
||
| 163 | |||
| 164 | # Return a slice of the path |
||
| 165 | # @return [KeyPath] |
||
| 166 | 1 | def [](index) |
|
| 167 | 202 | if index.is_a? Range |
|
| 168 | 1 | keys = @keys[index] |
|
| 169 | keys[0] = keys[0].entity.id_field \ |
||
| 170 | 1 | unless keys.empty? || keys[0].instance_of?(Fields::IDField) |
|
| 171 | 1 | KeyPath.new(keys) |
|
| 172 | else |
||
| 173 | 201 | key = @keys[index] |
|
| 174 | key = key.entity.id_field \ |
||
| 175 | 201 | unless key.nil? || key.instance_of?(Fields::IDField) |
|
| 176 | 201 | key |
|
| 177 | end |
||
| 178 | end |
||
| 179 | |||
| 180 | # Return the reverse of this path |
||
| 181 | # @return [KeyPath] |
||
| 182 | 1 | def reverse |
|
| 183 | 69 | KeyPath.new reverse_path |
|
| 184 | end |
||
| 185 | |||
| 186 | # Reverse this path in place |
||
| 187 | # @return [void] |
||
| 188 | 1 | def reverse! |
|
| 189 | @keys = reverse_path |
||
| 190 | end |
||
| 191 | |||
| 192 | # Simple wrapper so that we continue to be a KeyPath |
||
| 193 | # @return [KeyPath] |
||
| 194 | 1 | def to_a |
|
| 195 | self |
||
| 196 | end |
||
| 197 | |||
| 198 | # Return all the entities along the path |
||
| 199 | # @return [Array<Entity>] |
||
| 200 | 1 | def entities |
|
| 201 | 1124 | @entities ||= @keys.map(&:entity) |
|
| 202 | end |
||
| 203 | |||
| 204 | # Split the path where it intersects the given entity |
||
| 205 | # @return [KeyPath] |
||
| 206 | 1 | def split(entity) |
|
| 207 | if first.parent == entity |
||
| 208 | query_keys = KeyPath.new([entity.id_field]) |
||
| 209 | else |
||
| 210 | query_keys = [] |
||
| 211 | each do |key| |
||
| 212 | query_keys << key |
||
| 213 | break if key.is_a?(Fields::ForeignKeyField) && key.entity == entity |
||
| 214 | end |
||
| 215 | query_keys = KeyPath.new(query_keys) |
||
| 216 | end |
||
| 217 | end |
||
| 218 | |||
| 219 | # Find where the path intersects the given |
||
| 220 | # entity and splice in the target path |
||
| 221 | # @return [KeyPath] |
||
| 222 | 1 | def splice(target, entity) |
|
| 223 | split(entity) + target |
||
| 224 | end |
||
| 225 | |||
| 226 | # Get the named path to reach this field through the list of keys |
||
| 227 | # @return [Array<String>] |
||
| 228 | 1 | def path_for_field(field) |
|
| 229 | 56 | return [field.name] if @keys.first.parent == field.parent |
|
| 230 | |||
| 231 | 26 | @keys.each_cons(2).take_while do |prev_key, _| |
|
| 232 | 36 | prev_key.entity != field.parent |
|
| 233 | end.map(&:last).map(&:name) << field.name |
||
| 234 | end |
||
| 235 | |||
| 236 | # Find the parent of a given field |
||
| 237 | # @Return [Entity] |
||
| 238 | 1 | def find_field_parent(field) |
|
| 239 | 2 | parent = find do |key| |
|
| 240 | 2 | field.parent == key.parent || |
|
| 241 | (key.is_a?(Fields::ForeignKeyField) && field.parent == key.entity) |
||
| 242 | end |
||
| 243 | |||
| 244 | # This field is not on this portion of the path, so skip |
||
| 245 | 2 | return nil if parent.nil? |
|
| 246 | |||
| 247 | 2 | parent = parent.parent unless parent.is_a?(Fields::ForeignKeyField) |
|
| 248 | 2 | parent |
|
| 249 | end |
||
| 250 | |||
| 251 | # Produce all subpaths of this path |
||
| 252 | # @return [Enumerable<KeyPath>] |
||
| 253 | 1 | def subpaths(include_self = true) |
|
|
0 ignored issues
–
show
|
|||
| 254 | Enumerator.new do |enum| |
||
| 255 | enum.yield self if include_self |
||
| 256 | 1.upto(@keys.length) do |i| |
||
| 257 | i.upto(@keys.length) do |j| |
||
| 258 | enum.yield self[i - 1..j - 1] |
||
| 259 | end |
||
| 260 | end |
||
| 261 | end |
||
| 262 | end |
||
| 263 | |||
| 264 | 1 | private |
|
| 265 | |||
| 266 | # Get the reverse path |
||
| 267 | # @return [Array<Fields::Field>] |
||
| 268 | 1 | def reverse_path |
|
| 269 | 69 | return [] if @keys.empty? |
|
|
0 ignored issues
–
show
|
|||
| 270 | 69 | [@keys.last.entity.id_field] + @keys[1..-1].reverse.map(&:reverse) |
|
|
0 ignored issues
–
show
|
|||
| 271 | end |
||
| 272 | end |
||
| 273 | |||
| 274 | # A CQL statement and its associated data |
||
| 275 | 1 | class Statement |
|
| 276 | 1 | attr_reader :entity, :key_path, :label, :graph, |
|
| 277 | :group, :text, :eq_fields, :range_field, :comment |
||
| 278 | |||
| 279 | # Parse either a query or an update |
||
| 280 | 1 | def self.parse(text, model, group: nil, label: nil, support: false) |
|
| 281 | 246 | klass = statement_class text, support |
|
| 282 | 246 | tree = parse_tree text, klass |
|
| 283 | |||
| 284 | # Ensure we have a valid path in the parse tree |
||
| 285 | 246 | tree[:path] ||= [tree[:entity]] |
|
| 286 | fail InvalidStatementException, |
||
|
0 ignored issues
–
show
|
|||
| 287 | "FROM clause must start with #{tree[:entity]}" \ |
||
| 288 | 246 | if tree[:entity] && tree[:path].first != tree[:entity] |
|
| 289 | |||
| 290 | 245 | params = statement_parameters tree, model |
|
| 291 | 245 | statement = klass.parse tree, params, text, group: group, label: label |
|
| 292 | 241 | statement.instance_variable_set :@comment, tree[:comment].to_s |
|
| 293 | |||
| 294 | # Support queries need to populate extra values before finalizing |
||
| 295 | 241 | unless support |
|
| 296 | 241 | statement.hash |
|
| 297 | 241 | statement.freeze |
|
| 298 | end |
||
| 299 | |||
| 300 | 241 | statement |
|
| 301 | end |
||
| 302 | |||
| 303 | # Produce the class of the statement for the given text |
||
| 304 | # @return [Class, Symbol] |
||
| 305 | 1 | def self.statement_class(text, support) |
|
| 306 | 246 | return SupportQuery if support |
|
| 307 | |||
| 308 | 246 | case text.split.first |
|
| 309 | when 'INSERT' |
||
| 310 | 17 | Insert |
|
| 311 | when 'DELETE' |
||
| 312 | 9 | Delete |
|
| 313 | when 'UPDATE' |
||
| 314 | 24 | Update |
|
| 315 | when 'CONNECT' |
||
| 316 | 9 | Connect |
|
| 317 | when 'DISCONNECT' |
||
| 318 | 4 | Disconnect |
|
| 319 | else # SELECT |
||
| 320 | 183 | Query |
|
| 321 | end |
||
| 322 | end |
||
| 323 | 1 | private_class_method :statement_class |
|
| 324 | |||
| 325 | # Run the parser and produce the parse tree |
||
| 326 | # @raise [ParseFailed] |
||
| 327 | # @return [Hash] |
||
| 328 | 1 | def self.parse_tree(text, klass) |
|
| 329 | # Set the type of the statement |
||
| 330 | # (but CONNECT and DISCONNECT use the same parse rule) |
||
| 331 | 246 | type = klass.name.split('::').last.downcase.to_sym |
|
| 332 | 246 | type = :connect if type == :disconnect |
|
| 333 | |||
| 334 | # If parsing fails, re-raise as our custom exception |
||
| 335 | begin |
||
| 336 | 246 | tree = CQLT.new.apply(CQLP.new.method(type).call.parse(text)) |
|
| 337 | rescue Parslet::ParseFailed => exc |
||
|
0 ignored issues
–
show
|
|||
| 338 | new_exc = ParseFailed.new exc.cause.ascii_tree |
||
| 339 | new_exc.set_backtrace exc.backtrace |
||
| 340 | raise new_exc |
||
| 341 | end |
||
| 342 | |||
| 343 | 246 | tree |
|
| 344 | end |
||
| 345 | 1 | private_class_method :parse_tree |
|
| 346 | |||
| 347 | # Produce the parameter hash needed to build a new statement |
||
| 348 | # @return [Hash] |
||
| 349 | 1 | def self.statement_parameters(tree, model) |
|
| 350 | 245 | entity = model[tree[:path].first.to_s] |
|
| 351 | 245 | key_path = find_longest_path(tree[:path], entity) |
|
| 352 | |||
| 353 | { |
||
| 354 | 245 | model: model, |
|
| 355 | entity: entity, |
||
| 356 | key_path: key_path, |
||
| 357 | graph: QueryGraph::Graph.from_path(key_path) |
||
| 358 | } |
||
| 359 | end |
||
| 360 | 1 | private_class_method :statement_parameters |
|
| 361 | |||
| 362 | # Calculate the longest path of entities traversed by the statement |
||
| 363 | # @return [KeyPath] |
||
| 364 | 1 | def self.find_longest_path(path_entities, from) |
|
| 365 | 245 | path = path_entities.map(&:to_s)[1..-1] |
|
|
0 ignored issues
–
show
|
|||
| 366 | 245 | longest_entity_path = [from] |
|
| 367 | 245 | keys = [from.id_field] |
|
| 368 | |||
| 369 | 245 | path.each do |key| |
|
| 370 | # Search through foreign keys |
||
| 371 | 75 | last_entity = longest_entity_path.last |
|
| 372 | 75 | longest_entity_path << last_entity[key].entity |
|
| 373 | 75 | keys << last_entity[key] |
|
| 374 | end |
||
| 375 | |||
| 376 | 245 | KeyPath.new(keys) |
|
| 377 | end |
||
| 378 | 1 | private_class_method :find_longest_path |
|
| 379 | |||
| 380 | # A helper to look up a field based on the path specified in the statement |
||
| 381 | # @return [Fields::Field] |
||
| 382 | 1 | def self.add_field_with_prefix(path, field, params) |
|
|
0 ignored issues
–
show
|
|||
| 383 | 510 | field_path = field.map(&:to_s) |
|
| 384 | 510 | prefix_index = path.index(field_path.first) |
|
| 385 | field_path = path[0..prefix_index - 1] + field_path \ |
||
| 386 | 510 | unless prefix_index.zero? |
|
| 387 | 510 | field_path.map!(&:to_s) |
|
| 388 | |||
| 389 | # Expand the graph to include any keys which were found |
||
| 390 | 510 | field_path[0..-2].prefixes.drop(1).each do |key_path| |
|
| 391 | 95 | key = params[:model].find_field key_path |
|
| 392 | 95 | params[:graph].add_edge key.parent, key.entity, key |
|
| 393 | end |
||
| 394 | |||
| 395 | 510 | params[:model].find_field field_path |
|
| 396 | end |
||
| 397 | 1 | private_class_method :add_field_with_prefix |
|
| 398 | |||
| 399 | 1 | def initialize(params, text, group: nil, label: nil) |
|
| 400 | 327 | @entity = params[:entity] |
|
| 401 | 327 | @key_path = params[:key_path] |
|
| 402 | 327 | @longest_entity_path = @key_path.entities |
|
| 403 | 327 | @graph = params[:graph] |
|
| 404 | 327 | @model = params[:model] |
|
| 405 | 327 | @text = text |
|
| 406 | 327 | @group = group |
|
| 407 | 327 | @label = label |
|
| 408 | end |
||
| 409 | |||
| 410 | # Specifies if the statement modifies any data |
||
| 411 | # @return [Boolean] |
||
| 412 | 1 | def read_only? |
|
| 413 | false |
||
| 414 | end |
||
| 415 | |||
| 416 | # Specifies if the statement will require data to be inserted |
||
| 417 | # @return [Boolean] |
||
| 418 | 1 | def requires_insert?(_index) |
|
| 419 | 11 | false |
|
| 420 | end |
||
| 421 | |||
| 422 | # Specifies if the statement will require data to be deleted |
||
| 423 | # @return [Boolean] |
||
| 424 | 1 | def requires_delete?(_index) |
|
| 425 | 1 | false |
|
| 426 | end |
||
| 427 | |||
| 428 | # :nocov: |
||
| 429 | 1 | def to_color |
|
| 430 | 3 | "#{@text} [magenta]#{@longest_entity_path.map(&:name).join ', '}[/]" |
|
| 431 | end |
||
| 432 | # :nocov: |
||
| 433 | |||
| 434 | 1 | protected |
|
| 435 | |||
| 436 | # Quote the value of an identifier used as |
||
| 437 | # a value for a field, quoted if needed |
||
| 438 | # @return [String] |
||
| 439 | 1 | def maybe_quote(value, field) |
|
| 440 | 5 | if value.nil? |
|
| 441 | '?' |
||
| 442 | 5 | elsif [Fields::IDField, |
|
| 443 | Fields::ForeignKeyField, |
||
| 444 | Fields::StringField].include? field.class |
||
| 445 | 5 | "\"#{value}\"" |
|
| 446 | else |
||
| 447 | value.to_s |
||
| 448 | end |
||
| 449 | end |
||
| 450 | |||
| 451 | # Generate a string which can be used in the "FROM" clause |
||
| 452 | # of a statement or optionally to specify a field |
||
| 453 | # @return [String] |
||
| 454 | 1 | def from_path(path, prefix_path = nil, field = nil) |
|
|
0 ignored issues
–
show
|
|||
| 455 | 293 | if prefix_path.nil? |
|
| 456 | 93 | from = path.first.parent.name.dup |
|
| 457 | else |
||
| 458 | # Find where the two paths intersect to get the first path component |
||
| 459 | 200 | first_key = prefix_path.entries.find do |key| |
|
| 460 | 232 | path.entities.include?(key.parent) || \ |
|
| 461 | key.is_a?(Fields::ForeignKeyField) && \ |
||
| 462 | path.entities.include?(key.entity) |
||
| 463 | end |
||
| 464 | 200 | from = if first_key.primary_key? |
|
| 465 | 168 | first_key.parent.name.dup |
|
| 466 | else |
||
| 467 | 32 | first_key.name.dup |
|
| 468 | end |
||
| 469 | end |
||
| 470 | |||
| 471 | from << '.' << path.entries[1..-1].map(&:name).join('.') \ |
||
|
0 ignored issues
–
show
|
|||
| 472 | 293 | if path.length > 1 |
|
| 473 | |||
| 474 | 293 | unless field.nil? |
|
| 475 | 200 | from << '.' unless from.empty? |
|
| 476 | 200 | from << field.name |
|
| 477 | end |
||
| 478 | |||
| 479 | 293 | from |
|
| 480 | end |
||
| 481 | |||
| 482 | # Produce a string which can be used |
||
| 483 | # as the settings clause in a statement |
||
| 484 | # @return [String] |
||
| 485 | 1 | def settings_clause |
|
| 486 | 2 | 'SET ' + @settings.map do |setting| |
|
|
0 ignored issues
–
show
|
|||
| 487 | 3 | value = maybe_quote setting.value, setting.field |
|
| 488 | 3 | "#{setting.field.name} = #{value}" |
|
| 489 | end.join(', ') |
||
| 490 | end |
||
| 491 | |||
| 492 | # Produce a string which can be used |
||
| 493 | # as the WHERE clause in a statement |
||
| 494 | # @return [String] |
||
| 495 | 1 | def where_clause(field_namer = :to_s.to_proc) |
|
| 496 | 93 | ' WHERE ' + @conditions.values.map do |condition| |
|
|
0 ignored issues
–
show
|
|||
| 497 | 98 | value = condition.value.nil? ? '?' : condition.value |
|
| 498 | 98 | "#{field_namer.call condition.field} #{condition.operator} #{value}" |
|
| 499 | end.join(' AND ') |
||
| 500 | end |
||
| 501 | end |
||
| 502 | |||
| 503 | # The setting of a field from an {Update} statement |
||
| 504 | 1 | class FieldSetting |
|
| 505 | 1 | attr_reader :field, :value |
|
| 506 | |||
| 507 | 1 | def initialize(field, value) |
|
| 508 | 123 | @field = field |
|
| 509 | 123 | @value = value |
|
| 510 | |||
| 511 | 123 | freeze |
|
| 512 | end |
||
| 513 | |||
| 514 | 1 | def inspect |
|
| 515 | "#{@field.inspect} = #{value}" |
||
| 516 | end |
||
| 517 | |||
| 518 | # Compare settings equal by their field |
||
| 519 | 1 | def ==(other) |
|
| 520 | 5 | other.field == @field |
|
| 521 | end |
||
| 522 | 1 | alias eql? == |
|
| 523 | |||
| 524 | # Hash by field and value |
||
| 525 | 1 | def hash |
|
| 526 | 109 | Zlib.crc32 [@field.id, @value].to_s |
|
| 527 | end |
||
| 528 | end |
||
| 529 | |||
| 530 | # Module to add variable settings to a {Statement} |
||
| 531 | 1 | module StatementSettings |
|
| 532 | 1 | attr_reader :settings |
|
| 533 | |||
| 534 | 1 | def self.included(base) |
|
| 535 | 2 | base.extend ClassMethods |
|
| 536 | end |
||
| 537 | |||
| 538 | # Add methods to the class for populating settings |
||
| 539 | 1 | module ClassMethods |
|
| 540 | 1 | private |
|
| 541 | |||
| 542 | # Extract settings from a parse tree |
||
| 543 | # @return [Array<FieldSetting>] |
||
| 544 | 1 | def settings_from_tree(tree, params) |
|
| 545 | 40 | params[:settings] = tree[:settings].map do |setting| |
|
| 546 | 109 | field = params[:entity][setting[:field].to_s] |
|
| 547 | 109 | value = setting[:value] |
|
| 548 | |||
| 549 | 109 | type = field.class.const_get 'TYPE' |
|
| 550 | value = field.class.value_from_string(value.to_s) \ |
||
| 551 | 109 | unless type.nil? || value.nil? |
|
| 552 | |||
| 553 | 109 | setting.delete :value |
|
| 554 | 109 | FieldSetting.new field, value |
|
| 555 | end |
||
| 556 | end |
||
| 557 | end |
||
| 558 | end |
||
| 559 | |||
| 560 | # Extend {Statement} objects to allow them to generate support queries |
||
| 561 | 1 | module StatementSupportQuery |
|
| 562 | # Determine if this statement modifies a particular index |
||
| 563 | 1 | def modifies_index?(index) |
|
| 564 | 334 | !(@settings.map(&:field).to_set & index.all_fields).empty? |
|
| 565 | end |
||
| 566 | |||
| 567 | # Support queries required to updating the given index with this statement |
||
| 568 | # @return [Array<SupportQuery>] |
||
| 569 | 1 | def support_queries(_index) |
|
| 570 | [] |
||
| 571 | end |
||
| 572 | |||
| 573 | 1 | private |
|
| 574 | |||
| 575 | # Build a support query to update a given index |
||
| 576 | # and select fields with certain conditions |
||
| 577 | # @return [SupportQuery] |
||
| 578 | 1 | def build_support_query(entity, index, graph, select, conditions) |
|
| 579 | 87 | return nil if select.empty? |
|
| 580 | |||
| 581 | params = { |
||
| 582 | 84 | select: select, |
|
| 583 | graph: graph, |
||
| 584 | key_path: graph.longest_path, |
||
| 585 | entity: key_path.first.parent, |
||
| 586 | conditions: conditions |
||
| 587 | } |
||
| 588 | |||
| 589 | 84 | support_query = SupportQuery.new entity, params, nil, group: @group |
|
| 590 | 84 | support_query.instance_variable_set :@statement, self |
|
| 591 | 84 | support_query.instance_variable_set :@index, index |
|
| 592 | 84 | support_query.instance_variable_set :@comment, (hash ^ index.hash).to_s |
|
| 593 | 84 | support_query.instance_variable_set :@text, support_query.unparse |
|
| 594 | 84 | support_query.hash |
|
| 595 | 84 | support_query.freeze |
|
| 596 | end |
||
| 597 | |||
| 598 | # Produce support queries for the entity of the |
||
| 599 | # statement which select the given set of fields |
||
| 600 | # @return [Array<SupportQuery>] |
||
| 601 | 1 | def support_queries_for_entity(index, select) |
|
|
0 ignored issues
–
show
|
|||
| 602 | 59 | graphs = index.graph.size > 1 ? index.graph.split(entity, true) : [] |
|
| 603 | |||
| 604 | 59 | graphs.map do |graph| |
|
| 605 | 24 | support_fields = select.select do |field| |
|
| 606 | 58 | field.parent != entity && graph.entities.include?(field.parent) |
|
| 607 | end.to_set |
||
| 608 | |||
| 609 | conditions = { |
||
| 610 | 24 | entity.id_field.id => Condition.new(entity.id_field, :'=', nil) |
|
| 611 | } |
||
| 612 | |||
| 613 | 24 | split_entity = split_entity graph, index.graph, entity |
|
| 614 | 24 | build_support_query split_entity, index, graph, support_fields, |
|
| 615 | conditions |
||
| 616 | end.compact |
||
| 617 | end |
||
| 618 | |||
| 619 | # Determine which entity a subgraph was split at |
||
| 620 | # @return [Entity] |
||
| 621 | 1 | def split_entity(subgraph, graph, entity) |
|
| 622 | 28 | graph.keys_from_entity(entity).find do |key| |
|
| 623 | 28 | subgraph.entities.include? key.entity |
|
| 624 | end.entity |
||
| 625 | end |
||
| 626 | end |
||
| 627 | |||
| 628 | # Thrown when something tries to parse an invalid statement |
||
| 629 | 1 | class InvalidStatementException < StandardError |
|
| 630 | end |
||
| 631 | |||
| 632 | # Thrown when trying to construct a KeyPath which is not valid |
||
| 633 | 1 | class InvalidKeyPathException < StandardError |
|
| 634 | end |
||
| 635 | |||
| 636 | # Thrown when parsing a statement fails |
||
| 637 | 1 | class ParseFailed < StandardError |
|
| 638 | end |
||
| 639 | end |
||
| 640 | |||
| 641 | 1 | require_relative 'statements/connection' |
|
| 642 | 1 | require_relative 'statements/delete' |
|
| 643 | 1 | require_relative 'statements/insert' |
|
| 644 | 1 | require_relative 'statements/query' |
|
| 645 | require_relative 'statements/update' |
||
| 646 |