Module: Ancestry::MaterializedPath

Included in:
MaterializedPath2
Defined in:
lib/ancestry/materialized_path.rb

Overview

store ancestry as grandparent_id/parent_id root a=nil,id=1 children=id,id/% == 1, 1/% 3: a=1/2,id=3 children=a/id,a/id/% == 1/2/3, 1/2/3/%

Defined Under Namespace

Modules: InstanceMethods

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.child_ancestry_sql(table_name, ancestry_column, primary_key, delimiter, adapter) ⇒ Object



152
153
154
155
156
157
158
159
160
# File 'lib/ancestry/materialized_path.rb', line 152

def self.child_ancestry_sql(table_name, ancestry_column, primary_key, delimiter, adapter)
  pk_sql = concat(adapter, "#{table_name}.#{primary_key}")
  full_sql = concat(adapter, "#{table_name}.#{ancestry_column}", "'#{delimiter}'", "#{table_name}.#{primary_key}")
  %{
    CASE WHEN #{table_name}.#{ancestry_column} IS NULL THEN #{pk_sql}
    ELSE      #{full_sql}
    END
  }
end

.child_ancestry_value(ancestry_value, id, delimiter) ⇒ Object



126
127
128
# File 'lib/ancestry/materialized_path.rb', line 126

def self.child_ancestry_value(ancestry_value, id, delimiter)
  [ancestry_value, id].compact.join(delimiter)
end

.concat(adapter, *args) ⇒ Object



144
145
146
147
148
149
150
# File 'lib/ancestry/materialized_path.rb', line 144

def self.concat(adapter, *args)
  if %w(sqlite sqlite3).include?(adapter)
    args.join('||')
  else
    %{CONCAT(#{args.join(', ')})}
  end
end

.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter) ⇒ Object



162
163
164
165
166
# File 'lib/ancestry/materialized_path.rb', line 162

def self.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter)
  tmp = %{(LENGTH(#{table_name}.#{ancestry_column}) - LENGTH(REPLACE(#{table_name}.#{ancestry_column},'#{ancestry_delimiter}','')))}
  tmp += "/#{ancestry_delimiter.size}" if ancestry_delimiter.size > 1
  "(CASE WHEN #{table_name}.#{ancestry_column} IS NULL THEN 0 ELSE 1 + #{tmp} END)"
end

.descendants_condition(attr, child_ancestry, delimiter) ⇒ Object

Arel condition: descendants have ancestry matching child_ancestry or starting with child_ancestry/



131
132
133
# File 'lib/ancestry/materialized_path.rb', line 131

def self.descendants_condition(attr, child_ancestry, delimiter)
  attr.matches("#{child_ancestry}#{delimiter}%", nil, true).or(attr.eq(child_ancestry))
end

.extended(base) ⇒ Object



8
9
10
# File 'lib/ancestry/materialized_path.rb', line 8

def self.extended(base)
  base.send(:include, InstanceMethods)
end

.generate(ancestor_ids, delimiter, root) ⇒ Object



111
112
113
114
115
116
117
# File 'lib/ancestry/materialized_path.rb', line 111

def self.generate(ancestor_ids, delimiter, root)
  if ancestor_ids.present? && ancestor_ids.any?
    ancestor_ids.join(delimiter)
  else
    root
  end
end

.indirects_condition(attr, child_ancestry, delimiter) ⇒ Object

Arel condition: indirects have ancestry matching child_ancestry/*/



136
137
138
# File 'lib/ancestry/materialized_path.rb', line 136

def self.indirects_condition(attr, child_ancestry, delimiter)
  attr.matches("#{child_ancestry}#{delimiter}%", nil, true)
end

.parse(obj, root, delimiter, integer_pk) ⇒ Object



119
120
121
122
123
124
# File 'lib/ancestry/materialized_path.rb', line 119

def self.parse(obj, root, delimiter, integer_pk)
  return [] if obj.nil? || obj == root

  obj_ids = obj.split(delimiter).delete_if(&:blank?)
  integer_pk ? obj_ids.map!(&:to_i) : obj_ids
end

.validation_options(primary_key_format, delimiter) ⇒ Object



168
169
170
171
172
173
# File 'lib/ancestry/materialized_path.rb', line 168

def self.validation_options(primary_key_format, delimiter)
  {
    format: {with: /\A#{primary_key_format}(#{Regexp.escape(delimiter)}#{primary_key_format})*\z/.freeze},
    allow_nil: true
  }
end

Instance Method Details

#ancestors_of(object) ⇒ Object



20
21
22
23
24
# File 'lib/ancestry/materialized_path.rb', line 20

def ancestors_of(object)
  t = arel_table
  node = to_node(object)
  where(t[primary_key].in(node.ancestor_ids))
end

#ancestry_depth_change(old_value, new_value) ⇒ Object



107
108
109
# File 'lib/ancestry/materialized_path.rb', line 107

def ancestry_depth_change(old_value, new_value)
  parse_ancestry_column(new_value).size - parse_ancestry_column(old_value).size
end

#ancestry_depth_sqlObject



95
96
97
# File 'lib/ancestry/materialized_path.rb', line 95

def ancestry_depth_sql
  @ancestry_depth_sql ||= MaterializedPath.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter)
end

#ancestry_rootObject



87
88
89
# File 'lib/ancestry/materialized_path.rb', line 87

def ancestry_root
  nil
end

#child_ancestry_sqlObject



91
92
93
# File 'lib/ancestry/materialized_path.rb', line 91

def child_ancestry_sql
  MaterializedPath.child_ancestry_sql(table_name, ancestry_column, primary_key, ancestry_delimiter, connection.adapter_name.downcase)
end

#children_of(object) ⇒ Object



32
33
34
35
# File 'lib/ancestry/materialized_path.rb', line 32

def children_of(object)
  node = to_node(object)
  where(arel_table[ancestry_column].eq(node.child_ancestry))
end

#concat(*args) ⇒ Object



140
141
142
# File 'lib/ancestry/materialized_path.rb', line 140

def concat(*args)
  MaterializedPath.concat(connection.adapter_name.downcase, *args)
end

#descendant_before_last_save_conditions(object) ⇒ Object



55
56
57
58
# File 'lib/ancestry/materialized_path.rb', line 55

def descendant_before_last_save_conditions(object)
  node = to_node(object)
  descendants_by_ancestry(node.child_ancestry_before_last_save)
end

#descendant_conditions(object) ⇒ Object



50
51
52
53
# File 'lib/ancestry/materialized_path.rb', line 50

def descendant_conditions(object)
  node = to_node(object)
  descendants_by_ancestry(node.child_ancestry)
end

#descendants_by_ancestry(ancestry) ⇒ Object



46
47
48
# File 'lib/ancestry/materialized_path.rb', line 46

def descendants_by_ancestry(ancestry)
  MaterializedPath.descendants_condition(arel_table[ancestry_column], ancestry, ancestry_delimiter)
end

#descendants_of(object) ⇒ Object



42
43
44
# File 'lib/ancestry/materialized_path.rb', line 42

def descendants_of(object)
  where(descendant_conditions(object))
end

#generate_ancestry(ancestor_ids) ⇒ Object



99
100
101
# File 'lib/ancestry/materialized_path.rb', line 99

def generate_ancestry(ancestor_ids)
  MaterializedPath.generate(ancestor_ids, ancestry_delimiter, ancestry_root)
end

#indirects_of(object) ⇒ Object



37
38
39
40
# File 'lib/ancestry/materialized_path.rb', line 37

def indirects_of(object)
  node = to_node(object)
  where(MaterializedPath.indirects_condition(arel_table[ancestry_column], node.child_ancestry, ancestry_delimiter))
end

#inpath_of(object) ⇒ Object



26
27
28
29
30
# File 'lib/ancestry/materialized_path.rb', line 26

def inpath_of(object)
  t = arel_table
  node = to_node(object)
  where(t[primary_key].in(node.path_ids))
end

#ordered_by_ancestry(order = nil) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/ancestry/materialized_path.rb', line 70

def ordered_by_ancestry(order = nil)
  if %w(mysql mysql2 sqlite sqlite3).include?(connection.adapter_name.downcase)
    reorder(arel_table[ancestry_column], order)
  elsif %w(postgresql oracleenhanced).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::STRING >= "6.1"
    reorder(Arel::Nodes::Ascending.new(arel_table[ancestry_column]).nulls_first, order)
  else
    reorder(
      Arel::Nodes::Ascending.new(Arel::Nodes::NamedFunction.new('COALESCE', [arel_table[ancestry_column], Arel.sql("''")])),
      order
    )
  end
end

#ordered_by_ancestry_and(order) ⇒ Object



83
84
85
# File 'lib/ancestry/materialized_path.rb', line 83

def ordered_by_ancestry_and(order)
  ordered_by_ancestry(order)
end

#parse_ancestry_column(obj) ⇒ Object



103
104
105
# File 'lib/ancestry/materialized_path.rb', line 103

def parse_ancestry_column(obj)
  MaterializedPath.parse(obj, ancestry_root, ancestry_delimiter, primary_key_is_an_integer?)
end

#path_of(object) ⇒ Object



12
13
14
# File 'lib/ancestry/materialized_path.rb', line 12

def path_of(object)
  to_node(object).path
end

#rootsObject



16
17
18
# File 'lib/ancestry/materialized_path.rb', line 16

def roots
  where(arel_table[ancestry_column].eq(ancestry_root))
end

#siblings_of(object) ⇒ Object



65
66
67
68
# File 'lib/ancestry/materialized_path.rb', line 65

def siblings_of(object)
  node = to_node(object)
  where(arel_table[ancestry_column].eq(node[ancestry_column].presence))
end

#subtree_of(object) ⇒ Object



60
61
62
63
# File 'lib/ancestry/materialized_path.rb', line 60

def subtree_of(object)
  node = to_node(object)
  descendants_of(node).or(where(arel_table[primary_key].eq(node.id)))
end