Class: Exwiw::QueryAstBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/exwiw/query_ast_builder.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(table_name, table_by_name, dump_target, logger, allow_reverse: true, allow_forward: true) ⇒ QueryAstBuilder

Returns a new instance of QueryAstBuilder.



52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/exwiw/query_ast_builder.rb', line 52

def initialize(table_name, table_by_name, dump_target, logger, allow_reverse: true, allow_forward: true)
  @table_name = table_name
  @table_by_name = table_by_name
  @dump_target = dump_target
  @logger = logger
  @allow_reverse = allow_reverse
  # @allow_forward gates the "scope via an indirectly-scoped belongs_to
  # parent" rescue (build_belongs_to_scoped_clause). Disabled while building a
  # parent/child subquery so a single forward hop never recurses into another
  # (which could loop on a belongs_to cycle).
  @allow_forward = allow_forward
end

Instance Attribute Details

#dump_targetObject (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_nameObject (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_nameObject (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, allow_forward: true) ⇒ 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, allow_forward: true)
  new(table_name, table_by_name, dump_target, logger, allow_reverse: allow_reverse, allow_forward: allow_forward).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.

Returns:

  • (Boolean)


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).

Raises:

  • (ArgumentError)


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

#runObject



65
66
67
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
# File 'lib/exwiw/query_ast_builder.rb', line 65

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_categoryObject

Classifier used by validate_scope! and mirrored by build_scoped below.



457
458
459
460
461
462
463
464
465
466
# File 'lib/exwiw/query_ast_builder.rb', line 457

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 @allow_forward && build_belongs_to_scoped_clause(table)

  :unscopable
end