Class: ActiveRecord::Relation
Instance Attribute Summary collapse
-
#_brick_page_num ⇒ Object
Returns the value of attribute _brick_page_num.
Instance Method Summary collapse
-
#brick_links ⇒ Object
Links from ActiveRecord association pathing names over to real table correlation names that get chosen when the AREL AST tree is walked.
- #brick_select(params, selects = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new) ⇒ Object
Instance Attribute Details
#_brick_page_num ⇒ Object
Returns the value of attribute _brick_page_num.
410 411 412 |
# File 'lib/brick/extensions.rb', line 410 def _brick_page_num @_brick_page_num end |
Instance Method Details
#brick_links ⇒ Object
Links from ActiveRecord association pathing names over to real table correlation names that get chosen when the AREL AST tree is walked.
414 415 416 |
# File 'lib/brick/extensions.rb', line 414 def brick_links @brick_links ||= { '' => table_name } end |
#brick_select(params, selects = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new) ⇒ Object
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 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 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 |
# File 'lib/brick/extensions.rb', line 418 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 = ['Mysql2', 'Trilogy'].include?(ActiveRecord::Base.connection.adapter_name) is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' is_distinct = nil wheres = {} params.each do |k, v| next if ['_brick_schema', '_brick_order', '_brick_erd', '_brick_exclude', '_brick_unexclude', '_brick_page', '_brick_page_size', '_brick_offset', '_brick_limit', '_brick_is_api', 'controller', 'action'].include?(k) if (where_col = (ks = k.split('.')).last)[-1] == '!' where_col = where_col[0..-2] end case ks.length when 1 next unless klass.column_names.any?(where_col) || klass._brick_get_fks.include?(where_col) 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?(where_col) 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 id_parts = (id_col = klass.primary_key).is_a?(Array) ? id_col : [id_col] 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 is_api = params['_brick_is_api'] columns.each do |col| next if (col.type.nil? || col.type == :binary) && is_api 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 if is_distinct # Postgres can not use DISTINCT with any columns that are XML or JSON cast_as_text = if Brick.relations[klass.table_name]&.[](:cols)&.[](col_name)&.first == 'json' '::jsonb' # Convert JSON to JSONB elsif Brick.relations[klass.table_name]&.[](:cols)&.[](col_name)&.first&.start_with?('xml') '::text' # Convert XML to text end end "\"#{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) # Touching AREL AST walks the JoinDependency tree, and in that process uses our # "brick_links" patch to find how every AR chain of association names relates to exact # table correlation names chosen by AREL. We use a duplicate relation object for this # because an important side-effect of referencing the AST is that the @arel instance # variable gets set, and this is a signal to ActiveRecord that a relation has now # become immutable. (We aren't quite ready for our "real deal" relation object to be # set in stone ... still need to add .select(), and possibly .where() and .order() # things ... also if there are any HM counts then an OUTER JOIN for each of them out # to a derived table to do that counting. All of these things need to know proper # table correlation names, which will now become available in brick_links on the # rel_dupe object.) (rel_dupe = dup).arel.ast core_selects = selects.dup 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 key_klass = nil key_tbl_name = nil dest_pk = nil key_alias = nil cc.first.each do |cc_part| dest_klass = cc_part[0..-2].inject(klass) do |kl, cc_part_term| # %%% Clear column info properly so we can do multiple subsequent requests # binding.pry unless kl.reflect_on_association(cc_part_term) kl.reflect_on_association(cc_part_term)&.klass || klass end tbl_name = rel_dupe.brick_links[cc_part[0..-2].map(&:to_s).join('.')] # 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". if (cc_part_idx = cc_part.length - 1).zero? col_alias = "br_cc_#{k}__#{table_name.tr('.', '_')}" else while cc_part_idx > 0 && (col_alias = "br_cc_#{k}__#{cc_part[cc_part_idx..-1].map(&:to_s).join('__').tr('.', '_')}") && used_col_aliases.key?(col_alias) cc_part_idx -= 1 end end used_col_aliases[col_alias] = nil # Set up custom column links by preparing key_klass and key_alias # (If there are multiple different tables referenced in the DSL, we end up creating a link to the last one) if cc[2] && (dest_pk = dest_klass.primary_key) key_klass = dest_klass key_tbl_name = tbl_name cc_part_idx = cc_part.length - 1 while cc_part_idx > 0 && (key_alias = "br_cc_#{k}__#{(cc_part[cc_part_idx..-2] + [dest_pk]).map(&:to_s).join('__')}") && key_alias != col_alias && # We break out if this key alias does exactly match the col_alias used_col_aliases.key?(key_alias) cc_part_idx -= 1 end end selects << "#{tbl_name}.#{cc_part.last} AS #{col_alias}" cc_part << col_alias end # Add a key column unless we've already got it if key_alias && !used_col_aliases.key?(key_alias) selects << "#{key_tbl_name}.#{dest_pk} AS #{key_alias}" used_col_aliases[key_alias] = nil end cc[2] = key_alias ? [key_klass, key_alias] : nil 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 unless (tbl_name = rel_dupe.brick_links[v.first.to_s]&.split('.')&.last) # If it's Oracle, quote any AREL aliases that had been applied tbl_name = "\"#{tbl_name}\"" if ::Brick.is_oracle && rel_dupe.brick_links.values.include?(tbl_name) field_tbl_name = nil v1.map { |x| [x[0..-2].map(&:to_s).join('.'), x.last] }.each_with_index do |sel_col, idx| # %%% Strangely in Rails 7.1 on a slower system then very rarely brick_link comes back nil... brick_link = rel_dupe.brick_links[sel_col.first] field_tbl_name = brick_link&.split('.')&.last || # ... so here's a best-effort guess for what the table name might be. rel_dupe.klass.reflect_on_association(sel_col.first)&.klass&.table_name # If it's Oracle, quote any AREL aliases that had been applied field_tbl_name = "\"#{field_tbl_name}\"" if ::Brick.is_oracle && rel_dupe.brick_links.values.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[k1.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| next unless assoc_name.is_a?(Symbol) table_alias = rel_dupe.brick_links[assoc_name.to_s] _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 through_sources = [] # %%% Inverse path back to the original object -- not yet used, but soon # will be leveraged in order to build links with multi-table-hop filters. link_back = [] # Track polymorphic type field if necessary if hm.source_reflection.[:as] poly_ft = [hm.source_reflection.inverse_of.foreign_type, hmt_assoc.source_reflection.class_name] end # link_back << hm.source_reflection.inverse_of.name while hmt_assoc.[:through] && (hmt_assoc = klass.reflect_on_association(hmt_assoc.[:through])) through_sources.unshift(hmt_assoc) end # Turn the last member of link_back into a foreign key link_back << hmt_assoc.source_reflection.foreign_key # If it's a HMT based on a HM -> HM, must JOIN the last table into the mix at the end through_sources.push(hm.source_reflection) unless hm.source_reflection.belongs_to? from_clause = +"#{through_sources.first.table_name} br_t0" fk_col = through_sources.shift.foreign_key idx = 0 bail_out = nil through_sources.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 nm = hmt_assoc.source_reflection.inverse_of&.name link_back << nm "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] if a == hm.source_reflection print "Skipping #{hm.name} --HMT-> #{hm.source_reflection.name} as it uses source_type in a way which is not yet supported" nix << k bail_out = true break # "ON br_t#{idx}.#{a.foreign_type} = '#{src_ref.options[:source_type]}' AND " \ # "br_t#{idx}.#{a.foreign_key} = br_t#{idx - 1}.id" else # Works for HMT through a polymorphic HO link_back << hmt_assoc.source_reflection.inverse_of&.name # Some polymorphic "_able" thing "ON br_t#{idx - 1}.#{a.foreign_type} = '#{src_ref.[:source_type]}' AND " \ "br_t#{idx - 1}.#{a.foreign_key} = br_t#{idx}.id" end else # Standard has_many or has_one # binding.pry unless ( nm = hmt_assoc.source_reflection.inverse_of&.name # ) link_back << nm # if nm "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 # puts "LINK BACK! #{k} : #{hm.table_name} #{link_back.map(&:to_s).join('.')}" # 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 # binding.pry if link_back.length > 2 "br_t#{idx}.#{hm.foreign_key}" else # A HMT that goes HM -> HM, something like Categories -> Products -> LineItems # binding.pry if link_back.length > 2 "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 tbl_nm = hm.macro == :has_and_belongs_to_many ? hm.join_table : hm.table_name hm_table_name = if is_mysql "`#{tbl_nm}`" elsif is_postgres || is_mssql "\"#{(tbl_nm).gsub('.', '"."')}\"" else tbl_nm 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}" self.joins_values |= ["#{join_clause} ON #{on_clause.join(' AND ')}"] # Same as: joins!(...) 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 where_nots = {} wheres2 = wheres.each_with_object({}) do |v, s| is_not = if v.first[-1] == '!' v[0] = v[0][0..-2] # Take off ending ! from column name end if (v_parts = v.first.split('.')).length == 1 (is_not ? where_nots : s)[v.first] = v.last else tbl_name = rel_dupe.brick_links[v_parts.first].split('.').last (is_not ? where_nots : s)["#{tbl_name}.#{v_parts.last}"] = v.last end end if respond_to?(:where!) where!(wheres2) if wheres2.present? if where_nots.present? self.where_clause += WhereClause.new(predicate_builder.build_from_hash(where_nots)).invert end else # AR < 4.0 self.where_values << build_where(wheres2) end 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 self.order_values |= final_order_by # Same as: order!(*final_order_by) end # By default just 1000 rows row_limit = params['_brick_limit'] || params['_brick_page_size'] || 1000 offset = if (page = params['_brick_page']&.to_i) page = 1 if page < 1 (page - 1) * row_limit.to_i else params['_brick_offset'] end if offset.is_a?(Numeric) || offset&.present? offset = offset.to_i self.offset_value = offset unless offset == 0 @_brick_page_num = (offset / row_limit.to_i) + 1 if row_limit&.!= 0 && (offset % row_limit.to_i) == 0 end # Setting limit_value= is the same as doing: limit!(1000) but this way is compatible with AR <= 4.2 self.limit_value = row_limit.to_i unless row_limit.is_a?(String) && row_limit.empty? wheres unless wheres.empty? # Return the specific parameters that we did use end |