Module: SuperAuth::Nestable::ClassMethods
- Defined in:
- lib/super_auth/nestable.rb
Instance Method Summary collapse
-
#ancestor_pairs(of: nil) ⇒ Object
Every node paired with itself and each of its ancestors, as (descendant_id, ancestor_id).
-
#assert_acyclic! ⇒ Object
Refuses a table with a parent_id cycle in it, naming the nodes no root reaches.
- #base_name_path(base = self) ⇒ Object
- #base_path(base = self) ⇒ Object
- #cte(id = nil, direction = :desc) ⇒ Object
- #cte_name(base = self) ⇒ Object
-
#demodularize(base = self) ⇒ Object
See: ActiveSupport::Inflector.demodulize.
-
#descend_from(parent) ⇒ Object
Whether descendant_pairs continues below a node: a Sequel condition on the parent row, addressed through the alias
parent, or nil to descend from every node. -
#descendant_pairs(of: nil) ⇒ Object
Every node paired with itself and each of its descendants, as (ancestor_id, descendant_id).
-
#path_cast_type ⇒ Object
Cast type for the anchor row of the path CTEs.
- #pluralize(base = self) ⇒ Object
-
#rooted ⇒ Object
Every node some root reaches, walking parent_id downward from the rows that have none.
- #singularize(base = self) ⇒ Object
-
#string_cast_type ⇒ Object
Helper method to get the appropriate string cast type for the database.
- #with_ascending_paths(base_ds, recursive_ds, cte_name) ⇒ Object
- #with_descending_paths(base_ds, recursive_ds, cte_name) ⇒ Object
Instance Method Details
#ancestor_pairs(of: nil) ⇒ Object
Every node paired with itself and each of its ancestors, as (descendant_id, ancestor_id). The path strategies join these integer pairs on equality; matching ids inside the comma-separated path strings with LIKE forced a nested loop no planner could index, and compile time grew roughly cubically with the graph.
Both pair CTEs recurse with UNION rather than UNION ALL. The pair relation is finite (at most n² rows), so UNION stops as soon as a step produces nothing new, which on a parent_id cycle is the first time round; UNION ALL re-derives the same pairs forever and compile! never returns. On a valid tree no step repeats a pair, so the output is the same.
of: (a dataset or an array of ids) restricts the anchor to those
nodes, so only their ancestor chains are walked: one row per level.
103 104 105 106 107 108 109 110 111 |
# File 'lib/super_auth/nestable.rb', line 103 def ancestor_pairs(of: nil) table = pluralize name = :"#{singularize}_ancestor_pairs" anchor = db[table].select(Sequel[:id].as(:descendant_id), Sequel[:id].as(:ancestor_id)) anchor = anchor.where(id: of) unless of.nil? step = db[name].join(table, id: :ancestor_id).exclude(Sequel[table][:parent_id] => nil). select(Sequel[name][:descendant_id], Sequel[table][:parent_id]) db.from(name).with_recursive(name, anchor, step, args: [:descendant_id, :ancestor_id], union_all: false) end |
#assert_acyclic! ⇒ Object
Refuses a table with a parent_id cycle in it, naming the nodes no root reaches. compile! calls this for groups, roles and resources before touching the compiled table, because a cycle does not fail a compile: the walks terminate, and every node in the cycle is an ancestor of every other, so a grant on any of them silently reaches all of their subtrees. validate refuses the shape at the model; this catches it after a write that went around the model.
169 170 171 172 173 174 175 |
# File 'lib/super_auth/nestable.rb', line 169 def assert_acyclic! unreachable = dataset.exclude(id: rooted).select_order_map(:id) return if unreachable.empty? raise SuperAuth::Error, "#{pluralize} has a parent_id cycle: node(s) #{unreachable.join(', ')} " \ "cannot be reached from any root. Point one of them at a root, or at no parent, and recompile." end |
#base_name_path(base = self) ⇒ Object
274 275 276 |
# File 'lib/super_auth/nestable.rb', line 274 def base_name_path(base = self) "#{singularize(base)}_name_path".to_sym end |
#base_path(base = self) ⇒ Object
270 271 272 |
# File 'lib/super_auth/nestable.rb', line 270 def base_path(base = self) "#{singularize(base)}_path".to_sym end |
#cte(id = nil, direction = :desc) ⇒ Object
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/super_auth/nestable.rb', line 177 def cte(id = nil, direction = :desc) model = self cte_name = model.cte_name base_ds = model.select_all(pluralize) case direction when :asc base_ds = base_ds.where(id: id) recursive_ds = model .join(cte_name, parent_id: :id) .select_all(pluralize) base_ds, recursive_ds = with_ascending_paths(base_ds, recursive_ds, cte_name) when :desc if id base_ds = base_ds.where(id: id) else base_ds = base_ds.where(parent_id: id) end recursive_ds = model .join(cte_name, id: :parent_id) .select_all(pluralize(model)) base_ds, recursive_ds = with_descending_paths(base_ds, recursive_ds, cte_name) end model.from(cte_name) .with_recursive(cte_name, base_ds, recursive_ds) end |
#cte_name(base = self) ⇒ Object
266 267 268 |
# File 'lib/super_auth/nestable.rb', line 266 def cte_name(base = self) "super_auth_#{pluralize(base)}_cte".to_sym end |
#demodularize(base = self) ⇒ Object
See: ActiveSupport::Inflector.demodulize
250 251 252 253 254 255 256 |
# File 'lib/super_auth/nestable.rb', line 250 def demodularize(base = self) if i = base.name.rindex("::") base.name[(i + 2), base.name.length] else base.name end end |
#descend_from(parent) ⇒ Object
Whether descendant_pairs continues below a node: a Sequel condition on
the parent row, addressed through the alias parent, or nil to descend
from every node. Groups and roles descend from everything.
SuperAuth::Resource stops at a type-level node, so a grant on one yields
its own row and nothing beneath it on every path that reads the walk.
144 145 146 |
# File 'lib/super_auth/nestable.rb', line 144 def descend_from(parent) nil end |
#descendant_pairs(of: nil) ⇒ Object
Every node paired with itself and each of its descendants, as (ancestor_id, descendant_id). Granting a role grants its whole subtree.
of: (a dataset or an array of ids) restricts the anchor to those nodes,
so only their subtrees are walked. Groups and roles are few and the
whole table is cheap; resources are one row per protected record, and an
unanchored CTE materialises every pair of the whole table once per
strategy that joins it.
The recursive step joins the parent row only when the model puts a condition on it (descend_from); the pair CTE itself never carries more than the two ids.
125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/super_auth/nestable.rb', line 125 def descendant_pairs(of: nil) table = pluralize name = :"#{singularize}_descendant_pairs" parent = :"#{singularize}_parent" anchor = db[table].select(Sequel[:id].as(:ancestor_id), Sequel[:id].as(:descendant_id)) anchor = anchor.where(id: of) unless of.nil? step = db[name].join(table, parent_id: :descendant_id). select(Sequel[name][:ancestor_id], Sequel[table][:id]) if (condition = descend_from(parent)) step = step.join(Sequel[table].as(parent), id: Sequel[name][:descendant_id]).where(condition) end db.from(name).with_recursive(name, anchor, step, args: [:ancestor_id, :descendant_id], union_all: false) end |
#path_cast_type ⇒ Object
Cast type for the anchor row of the path CTEs. MySQL types a recursive CTE's columns from the anchor SELECT alone, so a bare CAST(id AS CHAR) makes the path column varchar(11) and every deeper level overflows it ("Data too long for column"). :text is unbounded elsewhere.
80 81 82 83 84 85 86 87 |
# File 'lib/super_auth/nestable.rb', line 80 def path_cast_type case SuperAuth.db.database_type when :mysql, :mysql2 "char(4000)" else :text end end |
#pluralize(base = self) ⇒ Object
258 259 260 |
# File 'lib/super_auth/nestable.rb', line 258 def pluralize(base = self) "super_auth_#{demodularize(base).downcase}s".to_sym end |
#rooted ⇒ Object
Every node some root reaches, walking parent_id downward from the rows that have none. On a valid forest that is the whole table; what it misses is exactly the nodes on or under a parent_id cycle (and, where no foreign key stands, a node whose parent is missing). No path columns, unlike trees: this runs over the whole resources table before every compile, and needs only the ids.
154 155 156 157 158 159 160 |
# File 'lib/super_auth/nestable.rb', line 154 def rooted table = pluralize name = :"rooted_#{table}" anchor = db[table].where(parent_id: nil).select(:id) step = db[name].join(table, parent_id: :id).select(Sequel[table][:id]) db.from(name).with_recursive(name, anchor, step, args: [:id], union_all: false) end |
#singularize(base = self) ⇒ Object
262 263 264 |
# File 'lib/super_auth/nestable.rb', line 262 def singularize(base = self) demodularize(base).downcase.to_sym end |
#string_cast_type ⇒ Object
Helper method to get the appropriate string cast type for the database
67 68 69 70 71 72 73 74 |
# File 'lib/super_auth/nestable.rb', line 67 def string_cast_type case SuperAuth.db.database_type when :mysql, :mysql2 :char else :text end end |
#with_ascending_paths(base_ds, recursive_ds, cte_name) ⇒ Object
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/super_auth/nestable.rb', line 230 def with_ascending_paths(base_ds, recursive_ds, cte_name) [ base_ds.select_append(Sequel[table_name][:id].cast(path_cast_type).as(base_path)).select_append(Sequel[table_name][:name].cast(path_cast_type).as(base_name_path)), recursive_ds.select_append( Sequel.function(:concat, Sequel[table_name][:id].cast(string_cast_type), Sequel.lit("','"), Sequel[cte_name][base_path].cast(string_cast_type), ).as(base_path) ).select_append( Sequel.function(:concat, Sequel[table_name][:name], Sequel.lit("','"), Sequel[cte_name][base_name_path], ).as(base_name_path) ) ] end |
#with_descending_paths(base_ds, recursive_ds, cte_name) ⇒ Object
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/super_auth/nestable.rb', line 208 def with_descending_paths(base_ds, recursive_ds, cte_name) [ base_ds.select_append( Sequel[table_name][:id].cast(path_cast_type).as(base_path) ).select_append(Sequel[table_name][:name].cast(path_cast_type).as(base_name_path)), recursive_ds.select_append( Sequel.function(:concat, Sequel[cte_name][base_path].cast(string_cast_type), Sequel.lit("','"), Sequel[pluralize][:id].cast(string_cast_type), ).as(base_path) ).select_append( Sequel.function(:concat, Sequel[cte_name][base_name_path], Sequel.lit("','"), Sequel[table_name][:name], ).as(base_name_path) ) ] end |