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.



39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/exwiw/query_ast_builder.rb', line 39

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.



37
38
39
# File 'lib/exwiw/query_ast_builder.rb', line 37

def dump_target
  @dump_target
end

#table_by_nameObject (readonly)

Returns the value of attribute table_by_name.



37
38
39
# File 'lib/exwiw/query_ast_builder.rb', line 37

def table_by_name
  @table_by_name
end

#table_nameObject (readonly)

Returns the value of attribute table_name.



37
38
39
# File 'lib/exwiw/query_ast_builder.rb', line 37

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

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


19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/exwiw/query_ast_builder.rb', line 19

def self.validate_scope!(tables, table_by_name, dump_target, logger)
  return if dump_target.scope_column.nil?

  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 by " \
        "'#{dump_target.scope_column}': #{names}. For each, add `scope_exempt: true` " \
        "to export it in full, set `ignore: true` to skip it, or add a belongs_to path " \
        "to a table that carries the scope column (use a per-table `scope_column` if the " \
        "column name differs on that table)."
end

Instance Method Details

#runObject



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/exwiw/query_ast_builder.rb', line 52

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.



377
378
379
380
381
382
383
384
385
386
# File 'lib/exwiw/query_ast_builder.rb', line 377

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