Class: ActiveRecord::Relation
Instance Attribute Summary collapse
-
#_arel_applied_aliases ⇒ Object
readonly
Returns the value of attribute _arel_applied_aliases.
-
#_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 = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new) ⇒ Object
Instance Attribute Details
#_arel_applied_aliases ⇒ Object (readonly)
Returns the value of attribute _arel_applied_aliases.
379 380 381 |
# File 'lib/brick/extensions.rb', line 379 def _arel_applied_aliases @_arel_applied_aliases end |
#_brick_chains ⇒ Object (readonly)
Returns the value of attribute _brick_chains.
379 380 381 |
# File 'lib/brick/extensions.rb', line 379 def _brick_chains @_brick_chains end |
Instance Method Details
#_arel_alias_names ⇒ Object
INSTANCE STUFF
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 |
# File 'lib/brick/extensions.rb', line 442 def _arel_alias_names @_arel_applied_aliases = [] # %%% 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
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 |
# File 'lib/brick/extensions.rb', line 382 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) @_arel_applied_aliases << (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 = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new) ⇒ Object
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 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 |
# File 'lib/brick/extensions.rb', line 458 def brick_select(params, selects = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new) is_add_bts = is_add_hms = true # Build out cust_cols, bt_descrip and hm_counts now so that they are available on the # model early in case the user wants to do an ORDER BY based on any of that. model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms is_postgres = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2' is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' is_distinct = nil wheres = {} params.each do |k, v| next if ['_brick_schema', '_brick_order', 'controller', 'action'].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 # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive # ActiveRecord::StatementInvalid (TinyTds::Error: DBPROCESS is dead or not enabled) # Relevant info here: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/402 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}" elsif is_postgres || is_mssql # 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}" elsif col.type # Could be Sqlite or Oracle if col_alias || !(/^[a-z0-9_]+$/ =~ col_name) "#{tbl_no_schema}.\"#{col_name}\"#{col_alias}" else "#{tbl_no_schema}.#{col_name}" end else # Oracle with a custom data type typ = col.sql_type "'<#{typ.end_with?('_TYP') ? typ[0..-5] : typ}>' AS #{col.name}" 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 # CUSTOM COLUMNS # ============== klass._br_cust_cols.each do |k, cc| if rel_dupe.respond_to?(k) # Name already taken? # %%% Use ensure_unique here in this kind of fashion: # cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms) # binding.pry next end cc.first.each do |cc_part| dest_klass = cc_part[0..-2].inject(klass) { |kl, cc_part_term| kl.reflect_on_association(cc_part_term).klass } tbl_name = (field_tbl_names[k][cc_part.last] ||= shift_or_first(chains[dest_klass])).split('.').last # Deal with the conflict if there are two parts in the custom column named the same, # "category.name" and "product.name" for instance will end up with aliases of "name" # and "product__name". cc_part_idx = cc_part.length - 1 while cc_part_idx > 0 && (col_alias = "br_cc_#{k}__#{cc_part[cc_part_idx..-1].map(&:to_s).join('__')}") && used_col_aliases.key?(col_alias) cc_part_idx -= 1 end selects << "#{tbl_name}.#{cc_part.last} AS #{col_alias}" cc_part << col_alias used_col_aliases[col_alias] = nil end end klass._br_bt_descrip.each do |v| 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 # If it's Oracle, quote any AREL aliases that had been applied tbl_name = "\"#{tbl_name}\"" if ::Brick.is_oracle && rel_dupe._arel_applied_aliases.include?(tbl_name) field_tbl_name = nil v1.map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx| # unless chains[sel_col.first] # puts 'You might have some bogus DSL in your brick.rb file' # next # end field_tbl_name = (field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])).split('.').last # If it's Oracle, quote any AREL aliases that had been applied field_tbl_name = "\"#{field_tbl_name}\"" if ::Brick.is_oracle && rel_dupe._arel_applied_aliases.include?(field_tbl_name) # 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 = "br_fk_#{v.first}__#{sel_col.last}") col_alias = "br_fk_#{v.first}__#{v1[idx][-2..-1].map(&:to_s).join('__')}" end selects << if is_mysql "`#{field_tbl_name}`.`#{sel_col.last}` AS `#{col_alias}`" elsif is_postgres "\"#{field_tbl_name}\".\"#{sel_col.last}\"#{'::text' if is_xml} AS \"#{col_alias}\"" elsif is_mssql "\"#{field_tbl_name}\".\"#{sel_col.last}\" AS \"#{col_alias}\"" else "#{field_tbl_name}.#{sel_col.last} 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 = "br_fk_#{v.first}__#{id_part}")}`" elsif is_postgres || is_mssql "#{"\"#{tbl_name}\".\"#{id_part}\""} AS \"#{(id_alias = "br_fk_#{v.first}__#{id_part}")}\"" else "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "br_fk_#{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 nix = [] klass._br_hm_counts.each do |k, hm| count_column = if hm.[:through] # Build the chain of JOINs going to the final destination HMT table # (Usually just one JOIN, but could be many.) hmt_assoc = hm x = [] x.unshift(hmt_assoc) while hmt_assoc.[:through] && (hmt_assoc = klass.reflect_on_association(hmt_assoc.[:through])) from_clause = +"#{x.first.table_name} br_t0" fk_col = x.shift.foreign_key link_back = [klass.primary_key] # %%% Inverse path back to the original object -- used to build out a link with a filter idx = 0 bail_out = nil x.map do |a| from_clause << "\n LEFT OUTER JOIN #{a.table_name} br_t#{idx += 1} " from_clause << if (src_ref = a.source_reflection).macro == :belongs_to "ON br_t#{idx}.id = br_t#{idx - 1}.#{a.foreign_key}" elsif src_ref.[:as] "ON br_t#{idx}.#{src_ref.type} = '#{src_ref.active_record.name}'" + # "polymorphable_type" " AND br_t#{idx}.#{src_ref.foreign_key} = br_t#{idx - 1}.id" elsif src_ref.[:source_type] print "Skipping #{hm.name} --HMT-> #{hm.source_reflection.name} as it uses source_type which is not supported" nix << k bail_out = true break else # Standard has_many "ON br_t#{idx}.#{a.foreign_key} = br_t#{idx - 1}.id" end link_back.unshift(a.source_reflection.name) [a.table_name, a.foreign_key, a.source_reflection.macro] end next if bail_out # count_column is determined from the originating HMT member if (src_ref = hm.source_reflection).nil? puts "*** Warning: Could not determine destination model for this HMT association in model #{klass.name}:\n has_many :#{hm.name}, through: :#{hm.[:through]}" puts nix << k next elsif src_ref.macro == :belongs_to # Traditional HMT using an associative table "br_t#{idx}.#{hm.foreign_key}" else # A HMT that goes HM -> HM, something like Categories -> Products -> LineItems "br_t#{idx}.#{src_ref.active_record.primary_key}" end else fk_col = (inv = hm.inverse_of)&.foreign_key || hm.foreign_key poly_type = inv.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 = if is_mysql "`b_r_#{hm.name}`" elsif is_postgres "\"b_r_#{hm.name}\"" else "b_r_#{hm.name}" end pri_tbl = hm.active_record pri_tbl_name = is_mysql ? "`#{pri_tbl.table_name}`" : "\"#{pri_tbl.table_name.gsub('.', '"."')}\"" pri_tbl_name = if is_mysql "`#{pri_tbl.table_name}`" elsif is_postgres || is_mssql "\"#{pri_tbl.table_name.gsub('.', '"."')}\"" else pri_tbl.table_name end on_clause = [] hm_selects = 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]}" } fk_col.dup else on_clause << "#{tbl_alias}.#{fk_col} = #{pri_tbl_name}.#{pri_tbl.primary_key}" [fk_col] end if poly_type hm_selects << poly_type on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'" end unless from_clause hm_table_name = if is_mysql "`#{hm.table_name}`" elsif is_postgres || is_mssql "\"#{(hm.table_name).gsub('.', '"."')}\"" else hm.table_name end end group_bys = ::Brick.is_oracle || is_mssql ? hm_selects : (1..hm_selects.length).to_a join_clause = "LEFT OUTER JOIN (SELECT #{hm_selects.map { |s| "#{'br_t0.' if from_clause}#{s}" }.join(', ')}, COUNT(#{'DISTINCT ' if hm.[:through]}#{count_column }) AS c_t_ FROM #{from_clause || hm_table_name} GROUP BY #{group_bys.join(', ')}) #{tbl_alias}" joins!("#{join_clause} ON #{on_clause.join(' AND ')}") end while (n = nix.pop) klass._br_hm_counts.delete(n) end unless wheres.empty? # Rewrite the wheres to reference table and correlation names built out by AREL wheres2 = wheres.each_with_object({}) do |v, s| if (v_parts = v.first.split('.')).length == 1 s[v.first] = v.last else k1 = klass.reflect_on_association(v_parts.first)&.klass tbl_name = (field_tbl_names[v_parts.first][k1] ||= shift_or_first(chains[k1])).split('.').last s["#{tbl_name}.#{v_parts.last}"] = v.last end end where!(wheres2) end # Must parse the order_by and see if there are any symbols which refer to BT associations # or custom columns as they must be expanded to find the corresponding b_r_model__column # or br_cc_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 elsif (cc_cols = klass._br_cust_cols[v]) cc_cols.first.each { |v1| s << "\"#{v1.last}\"" if v1.length > 1 } else s << v end else # String stuff (which defines a custom ORDER BY) just comes straight through s << v # Avoid "PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list" in Postgres selects << v if is_distinct 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 |