Class: Exwiw::QueryAstBuilder
- Inherits:
-
Object
- Object
- Exwiw::QueryAstBuilder
- Defined in:
- lib/exwiw/query_ast_builder.rb
Instance Attribute Summary collapse
-
#dump_target ⇒ Object
readonly
Returns the value of attribute dump_target.
-
#table_by_name ⇒ Object
readonly
Returns the value of attribute table_by_name.
-
#table_name ⇒ Object
readonly
Returns the value of attribute table_name.
Class Method Summary collapse
- .run(table_name, table_by_name, dump_target, logger, allow_reverse: true, forward_path: []) ⇒ Object
-
.scope_category(table_name, table_by_name, dump_target, logger) ⇒ Object
Scope-column mode classification for a single table.
-
.scope_mode?(table_by_name, dump_target) ⇒ Boolean
Scope-column mode is active when EITHER the named ‘–target-table` declares a per-table `scope_column` (the preferred trigger: the target is then scoped like any other table — its `–ids` are scope-column values, not primary keys), OR the deprecated `–scope-column` flag is set (a global column with no target).
-
.validate_scope!(tables, table_by_name, dump_target, logger) ⇒ Object
Strict pre-flight for scope-column mode: abort if any extractable table cannot be scoped, so an unscoped (potentially sensitive) table is never silently dumped in full.
Instance Method Summary collapse
-
#initialize(table_name, table_by_name, dump_target, logger, allow_reverse: true, forward_path: []) ⇒ QueryAstBuilder
constructor
A new instance of QueryAstBuilder.
- #run ⇒ Object
-
#scope_category ⇒ Object
Classifier used by validate_scope! and mirrored by build_scoped below.
Constructor Details
#initialize(table_name, table_by_name, dump_target, logger, allow_reverse: true, forward_path: []) ⇒ QueryAstBuilder
Returns a new instance of QueryAstBuilder.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/exwiw/query_ast_builder.rb', line 52 def initialize(table_name, table_by_name, dump_target, logger, allow_reverse: true, forward_path: []) @table_name = table_name @table_by_name = table_by_name @dump_target = dump_target @logger = logger @allow_reverse = allow_reverse # @forward_path is the chain of tables currently being forward-resolved by # the "scope via an indirectly-scoped belongs_to parent" rescue # (build_belongs_to_scoped_clause). Each forward hop appends the table it is # descending from, so the rescue recurses N levels (users -> end_users -> # end_user_profiles -> ...) and stops only on a real belongs_to cycle: a # table already on the path is not re-resolved, falling through to # :unscopable instead of looping forever. @forward_path = forward_path end |
Instance Attribute Details
#dump_target ⇒ Object (readonly)
Returns the value of attribute dump_target.
50 51 52 |
# File 'lib/exwiw/query_ast_builder.rb', line 50 def dump_target @dump_target end |
#table_by_name ⇒ Object (readonly)
Returns the value of attribute table_by_name.
50 51 52 |
# File 'lib/exwiw/query_ast_builder.rb', line 50 def table_by_name @table_by_name end |
#table_name ⇒ Object (readonly)
Returns the value of attribute table_name.
50 51 52 |
# File 'lib/exwiw/query_ast_builder.rb', line 50 def table_name @table_name end |
Class Method Details
.run(table_name, table_by_name, dump_target, logger, allow_reverse: true, forward_path: []) ⇒ Object
5 6 7 |
# File 'lib/exwiw/query_ast_builder.rb', line 5 def self.run(table_name, table_by_name, dump_target, logger, allow_reverse: true, forward_path: []) new(table_name, table_by_name, dump_target, logger, allow_reverse: allow_reverse, forward_path: forward_path).run end |
.scope_category(table_name, table_by_name, dump_target, logger) ⇒ Object
Scope-column mode classification for a single table. One of :exempt / :direct / :via_path / :referenced_by / :via_scoped_parent / :unscopable.
11 12 13 |
# File 'lib/exwiw/query_ast_builder.rb', line 11 def self.scope_category(table_name, table_by_name, dump_target, logger) new(table_name, table_by_name, dump_target, logger).scope_category end |
.scope_mode?(table_by_name, dump_target) ⇒ Boolean
Scope-column mode is active when EITHER the named ‘–target-table` declares a per-table `scope_column` (the preferred trigger: the target is then scoped like any other table — its `–ids` are scope-column values, not primary keys), OR the deprecated `–scope-column` flag is set (a global column with no target). In both cases every table is filtered by a shared column instead of being anchored on one named target’s primary key.
21 22 23 24 25 26 27 |
# File 'lib/exwiw/query_ast_builder.rb', line 21 def self.scope_mode?(table_by_name, dump_target) return true unless dump_target.scope_column.nil? return false if dump_target.table_name.nil? target = table_by_name[dump_target.table_name] !!(target && target.respond_to?(:scope_column) && target.scope_column) end |
.validate_scope!(tables, table_by_name, dump_target, logger) ⇒ Object
Strict pre-flight for scope-column mode: abort if any extractable table cannot be scoped, so an unscoped (potentially sensitive) table is never silently dumped in full. No-op outside scope mode. ‘tables` is the set of dumpable configs (ignore:true tables are skipped — they are not extracted).
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/exwiw/query_ast_builder.rb', line 33 def self.validate_scope!(tables, table_by_name, dump_target, logger) return unless scope_mode?(table_by_name, dump_target) unscopable = tables.reject(&:ignore).select do |table| scope_category(table.name, table_by_name, dump_target, logger) == :unscopable end return if unscopable.empty? names = unscopable.map(&:name).sort.join(", ") raise ArgumentError, "scope-column mode: #{unscopable.size} table(s) cannot be scoped: #{names}. " \ "For each, declare `scope_column: <column>` on the table to filter it directly, " \ "add a belongs_to path to a table that carries the scope column, mark it " \ "`scope_exempt: true` to export it in full, or set `ignore: true` to skip it." end |
Instance Method Details
#run ⇒ Object
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/exwiw/query_ast_builder.rb', line 68 def run table = table_by_name.fetch(table_name) return build_scoped(table) if scope_mode? where_clauses = build_where_clauses(table, dump_target) join_clauses = build_join_clauses(table, table_by_name, dump_target) # Reverse / "referenced_by" extraction. A table with no belongs_to path to # the dump target produces no where/join clauses and would otherwise dump # every row (see the "no relation -> dump all" case). If an extractable # child table references it via a foreign key (e.g. active_storage_blobs is # referenced by active_storage_attachments.blob_id), constrain it to just # the referenced ids instead. Disabled (@allow_reverse=false) while building # a child's subquery, so this never recurses. if @allow_reverse && table.name != dump_target.table_name && where_clauses.empty? && join_clauses.empty? reverse_clause = build_referenced_by_clause(table) where_clauses.push(reverse_clause) if reverse_clause end QueryAst::Select.new.tap do |ast| ast.from(table.name) if table.rails_managed? ast.select_all! else ast.select(table.columns) end join_clauses.each { |join_clause| ast.join(join_clause) } where_clauses.each { |where_clause| ast.where(where_clause) } end end |
#scope_category ⇒ Object
Classifier used by validate_scope! and mirrored by build_scoped below.
473 474 475 476 477 478 479 480 481 482 |
# File 'lib/exwiw/query_ast_builder.rb', line 473 def scope_category table = table_by_name.fetch(table_name) return :exempt if scope_exempt?(table) return :direct if directly_scoped?(table) return :via_path if build_join_clauses_scoped(table).any? return :referenced_by if @allow_reverse && build_referenced_by_clause(table) return :via_scoped_parent if forward_scope_allowed?(table) && build_belongs_to_scoped_clause(table) :unscopable end |