Class: ActiveRecord::Relation
Instance Attribute Summary collapse
-
#_brick_chains ⇒ Object
readonly
Returns the value of attribute _brick_chains.
Instance Method Summary collapse
-
#_arel_alias_names ⇒ Object
INSTANCE STUFF.
-
#_recurse_arel(piece, prefix = '') ⇒ Object
CLASS STUFF.
- #brick_select(params, selects = nil, order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new) ⇒ Object
Instance Attribute Details
#_brick_chains ⇒ Object (readonly)
Returns the value of attribute _brick_chains.
297 298 299 |
# File 'lib/brick/extensions.rb', line 297 def _brick_chains @_brick_chains end |
Instance Method Details
#_arel_alias_names ⇒ Object
INSTANCE STUFF
360 361 362 363 364 365 366 367 368 369 370 371 372 373 |
# File 'lib/brick/extensions.rb', line 360 def _arel_alias_names # %%% If with Rails 3.1 and older you get "NoMethodError: undefined method `eq' for nil:NilClass" # when trying to call relation.arel, then somewhere along the line while navigating a has_many # relationship it can't find the proper foreign key. core = arel.ast.cores.first # Accommodate AR < 3.2 if core.froms.is_a?(Arel::Table) # All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource _recurse_arel(core.source) else # With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin _recurse_arel(core.froms) end end |
#_recurse_arel(piece, prefix = '') ⇒ Object
CLASS STUFF
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 |
# File 'lib/brick/extensions.rb', line 300 def _recurse_arel(piece, prefix = '') names = [] # Our JOINs mashup of nested arrays and hashes # binding.pry if defined?(@arel) case piece when Array names += piece.inject([]) { |s, v| s + _recurse_arel(v, prefix) } when Hash names += piece.inject([]) do |s, v| new_prefix = "#{prefix}#{v.first}_" s << [v.last.shift, new_prefix] s + _recurse_arel(v.last, new_prefix) end # ActiveRecord AREL objects when Arel::Nodes::Join # INNER or OUTER JOIN # rubocop:disable Style/IdenticalConditionalBranches if piece.right.is_a?(Arel::Table) # Came in from AR < 3.2? # Arel 2.x and older is a little curious because these JOINs work "back to front". # The left side here is either another earlier JOIN, or at the end of the whole tree, it is # the first table. names += _recurse_arel(piece.left) # The right side here at the top is the very last table, and anywhere else down the tree it is # the later "JOIN" table of this pair. (The table that comes after all the rest of the JOINs # from the left side.) names << [piece.right._arel_table_type, (piece.right.table_alias || piece.right.name)] else # "Normal" setup, fed from a JoinSource which has an array of JOINs # The left side is the "JOIN" table names += _recurse_arel(table = piece.left) # The expression on the right side is the "ON" clause # on = piece.right.expr # # Find the table which is not ourselves, and thus must be the "path" that led us here # parent = piece.left == on.left.relation ? on.right.relation : on.left.relation # binding.pry if piece.left.is_a?(Arel::Nodes::TableAlias) if table.is_a?(Arel::Nodes::TableAlias) alias_name = table.right table = table.left end (_brick_chains[table._arel_table_type] ||= []) << (alias_name || table.table_alias || table.name) end # rubocop:enable Style/IdenticalConditionalBranches when Arel::Table # Table names << [piece._arel_table_type, (piece.table_alias || piece.name)] when Arel::Nodes::TableAlias # Alias # Can get the real table name from: self._recurse_arel(piece.left) names << [piece.left._arel_table_type, piece.right.to_s] # This is simply a string; the alias name itself when Arel::Nodes::JoinSource # Leaving this until the end because AR < 3.2 doesn't know at all about JoinSource! # Spin up an empty set of Brick alias name chains at the start @_brick_chains = {} # The left side is the "FROM" table names << (this_name = [piece.left._arel_table_type, (piece.left.table_alias || piece.left.name)]) # # Do not currently need the root "FROM" table in our list of chains # (_brick_chains[this_name.first] ||= []) << this_name.last # The right side is an array of all JOINs piece.right.each { |join| names << _recurse_arel(join) } end names end |
#brick_select(params, selects = nil, order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new) ⇒ Object
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 |
# File 'lib/brick/extensions.rb', line 375 def brick_select(params, selects = nil, order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new) is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2' is_distinct = nil wheres = {} params.each do |k, v| next if ['_brick_schema', '_brick_order'].include?(k) case (ks = k.split('.')).length when 1 next unless klass.column_names.any?(k) || klass._brick_get_fks.include?(k) when 2 assoc_name = ks.first.to_sym # Make sure it's a good association name and that the model has that column name next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last) join_array[assoc_name] = nil # Store this relation name in our special collection for .joins() is_distinct = true distinct! end wheres[k] = v.split(',') end # %%% Skip the metadata columns if selects&.empty? # Default to all columns tbl_no_schema = table.name.split('.').last columns.each do |col| col_alias = " AS _#{col.name}" if (col_name = col.name) == 'class' selects << if is_mysql "`#{tbl_no_schema}`.`#{col_name}`#{col_alias}" else # Postgres can not use DISTINCT with any columns that are XML, so for any of those just convert to text cast_as_text = '::text' if is_distinct && Brick.relations[klass.table_name]&.[](:cols)&.[](col.name)&.first&.start_with?('xml') "\"#{tbl_no_schema}\".\"#{col_name}\"#{cast_as_text}#{col_alias}" end end end if join_array.present? left_outer_joins!(join_array) # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable. (rel_dupe = dup)._arel_alias_names core_selects = selects.dup chains = rel_dupe._brick_chains id_for_tables = Hash.new { |h, k| h[k] = [] } field_tbl_names = Hash.new { |h, k| h[k] = {} } used_col_aliases = {} # Used to make sure there is not a name clash bt_columns = klass._br_bt_descrip.each_with_object([]) do |v, s| v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag next if chains[k1].nil? tbl_name = (field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])).split('.').last field_tbl_name = nil v1.map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx| field_tbl_name = (field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])).split('.').last # Postgres can not use DISTINCT with any columns that are XML, so for any of those just convert to text is_xml = is_distinct && Brick.relations[sel_col.first.table_name]&.[](:cols)&.[](sel_col.last)&.first&.start_with?('xml') # If it's not unique then also include the belongs_to association name before the column name if used_col_aliases.key?(col_alias = "_brfk_#{v.first}__#{sel_col.last}") col_alias = "_brfk_#{v.first}__#{v1[idx][-2..-1].map(&:to_s).join('__')}" end selects << if is_mysql "`#{field_tbl_name}`.`#{sel_col.last}` AS `#{col_alias}`" else "\"#{field_tbl_name}\".\"#{sel_col.last}\"#{'::text' if is_xml} AS \"#{col_alias}\"" end used_col_aliases[col_alias] = nil v1[idx] << col_alias end unless id_for_tables.key?(v.first) # Accommodate composite primary key by allowing id_col to come in as an array ((id_col = k1.primary_key).is_a?(Array) ? id_col : [id_col]).each do |id_part| id_for_tables[v.first] << if id_part selects << if is_mysql "#{"`#{tbl_name}`.`#{id_part}`"} AS `#{(id_alias = "_brfk_#{v.first}__#{id_part}")}`" else "#{"\"#{tbl_name}\".\"#{id_part}\""} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\"" end id_alias end end v1 << id_for_tables[v.first].compact end end end join_array.each do |assoc_name| # %%% Need to support {user: :profile} next unless assoc_name.is_a?(Symbol) table_alias = shift_or_first(chains[klass = reflect_on_association(assoc_name)&.klass]) _assoc_names[assoc_name] = [table_alias, klass] end end # Add derived table JOIN for the has_many counts klass._br_hm_counts.each do |k, hm| associative = nil count_column = if hm.[:through] hm.foreign_key if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key) else fk_col = hm.foreign_key poly_type = hm.inverse_of.foreign_type if hm..key?(:as) pk = hm.klass.primary_key (pk.is_a?(Array) ? pk.first : pk) || '*' end next unless count_column # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof tbl_alias = is_mysql ? "`_br_#{hm.name}`" : "\"_br_#{hm.name}\"" pri_tbl = hm.active_record pri_tbl_name = is_mysql ? "`#{pri_tbl.table_name}`" : "\"#{pri_tbl.table_name.gsub('.', '"."')}\"" on_clause = [] if fk_col.is_a?(Array) # Composite key? fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{tbl_alias}.#{fk_col_part} = #{pri_tbl_name}.#{pri_tbl.primary_key[idx]}" } selects = fk_col.dup else selects = [fk_col] on_clause << "#{tbl_alias}.#{fk_col} = #{pri_tbl_name}.#{pri_tbl.primary_key}" end if poly_type selects << poly_type on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'" end hm_table_name = is_mysql ? "`#{associative&.table_name || hm.klass.table_name}`" : "\"#{(associative&.table_name || hm.klass.table_name).gsub('.', '"."')}\"" join_clause = "LEFT OUTER JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.[:through]}#{count_column }) AS _ct_ FROM #{hm_table_name} GROUP BY #{(1..selects.length).to_a.join(', ')}) AS #{tbl_alias}" joins!("#{join_clause} ON #{on_clause.join(' AND ')}") end where!(wheres) unless wheres.empty? # Must parse the order_by and see if there are any symbols which refer to BT associations # as they must be expanded to find the corresponding _br_model__column naming for each. if order_by.present? final_order_by = *order_by.each_with_object([]) do |v, s| if v.is_a?(Symbol) # Add the ordered series of columns derived from the BT based on its DSL if (bt_cols = klass._br_bt_descrip[v]) bt_cols.values.each do |v1| v1.each { |v2| s << v2.last if v2.length > 1 } end else s << v end else # String stuff just comes straight through s << v end end order!(*final_order_by) end limit!(1000) # Don't want to get too carried away just yet wheres unless wheres.empty? # Return the specific parameters that we did use end |