exwiw 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/exe/exwiw +6 -0
- data/lib/exwiw/adapter/mysql2_adapter.rb +171 -0
- data/lib/exwiw/adapter/postgresql_adapter.rb +171 -0
- data/lib/exwiw/adapter/sqlite3_adapter.rb +165 -0
- data/lib/exwiw/adapter.rb +44 -0
- data/lib/exwiw/belongs_to.rb +14 -0
- data/lib/exwiw/cli.rb +150 -0
- data/lib/exwiw/determine_table_processing_order.rb +42 -0
- data/lib/exwiw/query_ast.rb +84 -0
- data/lib/exwiw/query_ast_builder.rb +125 -0
- data/lib/exwiw/railtie.rb +9 -0
- data/lib/exwiw/runner.rb +87 -0
- data/lib/exwiw/table_column.rb +19 -0
- data/lib/exwiw/table_config.rb +93 -0
- data/lib/exwiw/version.rb +5 -0
- data/lib/exwiw.rb +30 -0
- data/lib/tasks/exwiw.rake +54 -0
- metadata +79 -0
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exwiw
|
4
|
+
module Adapter
|
5
|
+
class Sqlite3Adapter < Base
|
6
|
+
def execute(query_ast)
|
7
|
+
sql = compile_ast(query_ast)
|
8
|
+
|
9
|
+
@logger.debug(" Executing SQL: \n#{sql}")
|
10
|
+
connection.execute(sql)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_bulk_insert(results, table)
|
14
|
+
table_name = table.name
|
15
|
+
|
16
|
+
value_list = results.map do |row|
|
17
|
+
quoted_values = row.map do |value|
|
18
|
+
escape_value(value)
|
19
|
+
end
|
20
|
+
"(" + quoted_values.join(', ') + ")"
|
21
|
+
end
|
22
|
+
values = value_list.join(",\n")
|
23
|
+
|
24
|
+
column_names = table.columns.map(&:name).join(', ')
|
25
|
+
"INSERT INTO #{table_name} (#{column_names}) VALUES\n#{values};"
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_bulk_delete(select_query_ast, table)
|
29
|
+
raise NotImplementedError unless select_query_ast.is_a?(Exwiw::QueryAst::Select)
|
30
|
+
|
31
|
+
sql = "DELETE FROM #{select_query_ast.from_table_name}"
|
32
|
+
|
33
|
+
if select_query_ast.join_clauses.empty?
|
34
|
+
# Ignore filter option, because bulk delete is for cleaning before import,
|
35
|
+
# so it should delete all records to avoid foreign key violation & data consistancy.
|
36
|
+
compiled_where_conditions = select_query_ast.
|
37
|
+
where_clauses.
|
38
|
+
select { |where| where.is_a?(Exwiw::QueryAst::WhereClause) }.
|
39
|
+
map do |where|
|
40
|
+
compile_where_condition(where, select_query_ast.from_table_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
if compiled_where_conditions.size > 0
|
44
|
+
sql += "\nWHERE "
|
45
|
+
sql += compiled_where_conditions.join(' AND ')
|
46
|
+
end
|
47
|
+
sql += ";"
|
48
|
+
|
49
|
+
return sql
|
50
|
+
end
|
51
|
+
|
52
|
+
subquery_ast = Exwiw::QueryAst::Select.new
|
53
|
+
first_join = select_query_ast.join_clauses.first.clone
|
54
|
+
|
55
|
+
subquery_ast.from(first_join.join_table_name)
|
56
|
+
primay_key_col = table.columns.find { |col| col.name == table.primary_key }
|
57
|
+
subquery_ast.select([primay_key_col])
|
58
|
+
select_query_ast.join_clauses[1..].each do |join|
|
59
|
+
subquery_ast.join(join)
|
60
|
+
end
|
61
|
+
first_join.where_clauses.each do |where|
|
62
|
+
# Ignore filter option, because bulk delete is for cleaning before import,
|
63
|
+
# so it should delete all records to avoid foreign key violation & data consistancy.
|
64
|
+
subquery_ast.where(where) if where.is_a?(Exwiw::QueryAst::WhereClause)
|
65
|
+
end
|
66
|
+
|
67
|
+
foreign_key = first_join.foreign_key
|
68
|
+
subquery_sql = compile_ast(subquery_ast)
|
69
|
+
sql += "\nWHERE #{select_query_ast.from_table_name}.#{foreign_key} IN (#{subquery_sql});"
|
70
|
+
|
71
|
+
sql
|
72
|
+
end
|
73
|
+
|
74
|
+
def compile_ast(query_ast)
|
75
|
+
raise NotImplementedError unless query_ast.is_a?(Exwiw::QueryAst::Select)
|
76
|
+
|
77
|
+
sql = "SELECT "
|
78
|
+
sql += query_ast.columns.map { |col| compile_column_name(query_ast, col) }.join(', ')
|
79
|
+
sql += " FROM #{query_ast.from_table_name}"
|
80
|
+
|
81
|
+
query_ast.join_clauses.each do |join|
|
82
|
+
sql += " JOIN #{join.join_table_name} ON #{query_ast.from_table_name}.#{join.foreign_key} = #{join.join_table_name}.#{join.primary_key}"
|
83
|
+
|
84
|
+
join.where_clauses.each do |where|
|
85
|
+
compiled_where_condition = compile_where_condition(where, join.join_table_name)
|
86
|
+
sql += " AND #{compiled_where_condition}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
if query_ast.where_clauses.any?
|
91
|
+
sql += " WHERE "
|
92
|
+
sql += query_ast.where_clauses.map { |where| compile_where_condition(where, query_ast.from_table_name) }.join(' AND ')
|
93
|
+
end
|
94
|
+
|
95
|
+
sql
|
96
|
+
end
|
97
|
+
|
98
|
+
private def compile_where_condition(where_clause, table_name)
|
99
|
+
# Use as it is if it's a raw query
|
100
|
+
return where_clause if where_clause.is_a?(String)
|
101
|
+
|
102
|
+
key = "#{table_name}.#{where_clause.column_name}"
|
103
|
+
|
104
|
+
if where_clause.operator == :eq
|
105
|
+
values = where_clause.value.map { |v| escape_value(v) }
|
106
|
+
|
107
|
+
if values.size == 1
|
108
|
+
"#{key} = #{values.first}"
|
109
|
+
else
|
110
|
+
"#{key} IN (#{values.join(', ')})"
|
111
|
+
end
|
112
|
+
else
|
113
|
+
raise "Unsupported operator: #{where_clause.operator}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private def escape_value(value)
|
118
|
+
case value
|
119
|
+
when nil
|
120
|
+
"NULL"
|
121
|
+
when String
|
122
|
+
qv = escape_single_quote(value)
|
123
|
+
"'#{qv}'"
|
124
|
+
else
|
125
|
+
value
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private def escape_single_quote(value)
|
130
|
+
value.gsub("'", "''")
|
131
|
+
end
|
132
|
+
|
133
|
+
private def compile_column_name(ast, column)
|
134
|
+
case column
|
135
|
+
when Exwiw::QueryAst::ColumnValue::Plain
|
136
|
+
"#{ast.from_table_name}.#{column.name}"
|
137
|
+
when Exwiw::QueryAst::ColumnValue::RawSql
|
138
|
+
column.value
|
139
|
+
when Exwiw::QueryAst::ColumnValue::ReplaceWith
|
140
|
+
parts = column.value.scan(/[^{}]+|\{[^{}]*\}/).map do |part|
|
141
|
+
if part.start_with?('{')
|
142
|
+
name = part[1..-2]
|
143
|
+
"#{ast.from_table_name}.#{name}"
|
144
|
+
else
|
145
|
+
"'#{part}'"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
replaced = parts.join(" || ")
|
150
|
+
"(#{replaced})"
|
151
|
+
else
|
152
|
+
raise "Unreachable case: #{column.inspect}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
private def connection
|
157
|
+
@connection ||=
|
158
|
+
begin
|
159
|
+
require 'sqlite3'
|
160
|
+
SQLite3::Database.new(@connection_config.database_name)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exwiw
|
4
|
+
module Adapter
|
5
|
+
class Base
|
6
|
+
attr_reader :connection_config
|
7
|
+
|
8
|
+
def initialize(connection_config, logger)
|
9
|
+
@connection_config = connection_config
|
10
|
+
@logger = logger
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# @params [Exwiw::QueryAst] query_ast
|
15
|
+
def execute(query_ast)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
# @params [Array<Array<String>>] array of rows
|
20
|
+
# @params [Exwiw::TableConfig] table
|
21
|
+
def to_bulk_insert(results, table)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
# @params [Exwiw::QueryAst] select_query_ast
|
26
|
+
# @params [Exwiw::TableConfig] table
|
27
|
+
def to_bulk_delete(select_query_ast, table)
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.build(connection_config, logger)
|
32
|
+
case connection_config.adapter
|
33
|
+
when 'sqlite3'
|
34
|
+
Adapter::Sqlite3Adapter.new(connection_config, logger)
|
35
|
+
when 'mysql2'
|
36
|
+
Adapter::Mysql2Adapter.new(connection_config, logger)
|
37
|
+
when 'postgresql'
|
38
|
+
Adapter::PostgresqlAdapter.new(connection_config, logger)
|
39
|
+
else
|
40
|
+
raise 'Unsupported adapter'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/exwiw/cli.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'optparse'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
require 'exwiw'
|
10
|
+
|
11
|
+
module Exwiw
|
12
|
+
class CLI
|
13
|
+
def self.start(argv)
|
14
|
+
new(argv).run
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(argv)
|
18
|
+
@argv = argv.dup
|
19
|
+
@help = argv.empty?
|
20
|
+
|
21
|
+
@database_host = nil
|
22
|
+
@database_port = nil
|
23
|
+
@database_user = nil
|
24
|
+
@database_password = ENV["DATABASE_PASSWORD"]
|
25
|
+
@output_dir = "dump"
|
26
|
+
@config_dir = nil
|
27
|
+
@database_adapter = nil
|
28
|
+
@database_name = nil
|
29
|
+
@target_table_name = nil
|
30
|
+
@ids = []
|
31
|
+
@log_level = :info
|
32
|
+
|
33
|
+
parser.parse!(@argv)
|
34
|
+
end
|
35
|
+
|
36
|
+
def run
|
37
|
+
if @help
|
38
|
+
puts parser.help
|
39
|
+
else
|
40
|
+
validate_options!
|
41
|
+
|
42
|
+
connection_config = ConnectionConfig.new(
|
43
|
+
adapter: @database_adapter,
|
44
|
+
host: @database_host,
|
45
|
+
port: @database_port,
|
46
|
+
user: @database_user,
|
47
|
+
password: @database_password,
|
48
|
+
database_name: @database_name,
|
49
|
+
)
|
50
|
+
|
51
|
+
dump_target = DumpTarget.new(
|
52
|
+
table_name: @target_table_name,
|
53
|
+
ids: @ids,
|
54
|
+
)
|
55
|
+
|
56
|
+
logger = build_logger
|
57
|
+
|
58
|
+
Runner.new(
|
59
|
+
connection_config: connection_config,
|
60
|
+
output_dir: @output_dir,
|
61
|
+
config_dir: @config_dir,
|
62
|
+
dump_target: dump_target,
|
63
|
+
logger: logger,
|
64
|
+
).run
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private def validate_options!
|
69
|
+
if @database_adapter != "sqlite3"
|
70
|
+
{
|
71
|
+
"Target database host" => @database_host,
|
72
|
+
"Target database port" => @database_port,
|
73
|
+
"Database user" => @database_user,
|
74
|
+
"Target database name" => @database_name,
|
75
|
+
}.each do |k, v|
|
76
|
+
if v.nil?
|
77
|
+
$stderr.puts "#{k} is required"
|
78
|
+
exit 1
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
if @config_dir.nil?
|
83
|
+
$stderr.puts "Config dir is required"
|
84
|
+
end
|
85
|
+
|
86
|
+
if @database_password.nil? || @database_password.empty?
|
87
|
+
$stderr.puts "environment variable 'DATABASE_PASSWORD' is required"
|
88
|
+
exit 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
valid_adapters = ["mysql2", "postgresql", "sqlite3"]
|
93
|
+
unless valid_adapters.include?(@database_adapter)
|
94
|
+
$stderr.puts "Invalid adapter. Available options are: #{valid_adapters.join(', ')}"
|
95
|
+
exit 1
|
96
|
+
end
|
97
|
+
|
98
|
+
if @target_table_name.nil? || @target_table_name.empty?
|
99
|
+
$stderr.puts "Target table is required"
|
100
|
+
exit 1
|
101
|
+
end
|
102
|
+
|
103
|
+
if @ids.empty?
|
104
|
+
$stderr.puts "At least one ID is required"
|
105
|
+
exit 1
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private def build_logger
|
110
|
+
formatter = proc do |severity, timestamp, progname, msg|
|
111
|
+
formatted_ts = timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
112
|
+
"#{formatted_ts} [#{progname}]: #{msg}\n"
|
113
|
+
end
|
114
|
+
|
115
|
+
Logger.new(
|
116
|
+
STDOUT,
|
117
|
+
level: @log_level,
|
118
|
+
datetime_format: "%Y-%m-%d %H:%M:%S",
|
119
|
+
progname: "exwiw",
|
120
|
+
formatter: formatter,
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
private def parser
|
125
|
+
@parser ||= OptionParser.new do |opts|
|
126
|
+
opts.banner = "exwiw #{Exwiw::VERSION}"
|
127
|
+
opts.version = Exwiw::VERSION
|
128
|
+
|
129
|
+
opts.on("-h", "--host=HOST", "Target database host") { |v| @database_host = v }
|
130
|
+
opts.on("-p", "--port=PORT", "Target database port") { |v| @database_port = v }
|
131
|
+
opts.on("-u", "--user=USERNAME", "Target database user") { |v| @database_user = v }
|
132
|
+
opts.on("-o", "--output-dir=[DUMP_DIR_PATH]", "Output file path. default is dump/") do |v|
|
133
|
+
@output_dir = v.end_with?("/") ? v[0..-2] : v
|
134
|
+
end
|
135
|
+
opts.on("-c", "--config-dir=CONFIG_DIR_PATH", "Config dir path.") do |v|
|
136
|
+
@config_dir = v.end_with?("/") ? v[0..-2] : v
|
137
|
+
end
|
138
|
+
opts.on("-a", "--adapter=ADAPTER", "Database adapter") { |v| @database_adapter = v }
|
139
|
+
opts.on("--database=DATABASE", "Target database name") { |v| @database_name = v }
|
140
|
+
opts.on("--target-table=TABLE", "Target table for extraction") { |v| @target_table_name = v }
|
141
|
+
opts.on("--ids=IDS", "Comma-separated list of identifiers") { |v| @ids = v.split(',') }
|
142
|
+
opts.on("--log-level=LEVEL", "Log level (debug, info). default is info") { |v| @log_level = v.to_sym }
|
143
|
+
|
144
|
+
opts.on("--help", "Print this help") do
|
145
|
+
@help = true
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exwiw
|
4
|
+
module DetermineTableProcessingOrder
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# @param tables [Array<Exwiw::TableConfig>] tables
|
8
|
+
# @return [Array<String>] sorted table names
|
9
|
+
def run(tables)
|
10
|
+
return tables.map(&:name) if tables.size < 2
|
11
|
+
|
12
|
+
ordered_table_names = []
|
13
|
+
|
14
|
+
table_by_name = tables.each_with_object({}) do |table, acc|
|
15
|
+
acc[table.name] = table
|
16
|
+
end
|
17
|
+
|
18
|
+
loop do
|
19
|
+
break if table_by_name.empty?
|
20
|
+
|
21
|
+
tables_with_no_dependencies = table_by_name.values.select do |table|
|
22
|
+
not_resolved_names = compute_table_dependencies(table) - ordered_table_names - [table.name]
|
23
|
+
|
24
|
+
not_resolved_names.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
tables_with_no_dependencies.each do |table|
|
28
|
+
ordered_table_names << table.name
|
29
|
+
table_by_name.delete(table.name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
ordered_table_names
|
34
|
+
end
|
35
|
+
|
36
|
+
def compute_table_dependencies(table)
|
37
|
+
table.belongs_tos.each_with_object([]) do |relation, acc|
|
38
|
+
acc << relation.table_name
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exwiw
|
4
|
+
module QueryAst
|
5
|
+
class JoinClause
|
6
|
+
attr_reader :base_table_name, :foreign_key, :join_table_name, :primary_key, :where_clauses
|
7
|
+
|
8
|
+
def initialize(base_table_name:, foreign_key:, join_table_name:, primary_key:, where_clauses: [])
|
9
|
+
@base_table_name = base_table_name
|
10
|
+
@foreign_key = foreign_key
|
11
|
+
@join_table_name = join_table_name
|
12
|
+
@primary_key = primary_key
|
13
|
+
@where_clauses = where_clauses
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_h
|
17
|
+
hash = {
|
18
|
+
base_table_name: base_table_name,
|
19
|
+
foreign_key: foreign_key,
|
20
|
+
join_table_name: join_table_name,
|
21
|
+
primary_key: primary_key,
|
22
|
+
}
|
23
|
+
hash[:where_clauses] = where_clauses.map(&:to_h) if where_clauses.size.positive?
|
24
|
+
hash
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
WhereClause = Struct.new(:column_name, :operator, :value, keyword_init: true) do
|
29
|
+
def to_h
|
30
|
+
{
|
31
|
+
column_name: column_name,
|
32
|
+
operator: operator,
|
33
|
+
value: value
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module ColumnValue
|
39
|
+
Base = Struct.new(:name, :value, keyword_init: true)
|
40
|
+
Plain = Class.new(Base)
|
41
|
+
ReplaceWith = Class.new(Base)
|
42
|
+
RawSql = Class.new(Base)
|
43
|
+
end
|
44
|
+
|
45
|
+
class Select
|
46
|
+
attr_reader :from_table_name, :columns, :where_clauses, :join_clauses
|
47
|
+
|
48
|
+
def initialize
|
49
|
+
@from_table_name = nil
|
50
|
+
@columns = []
|
51
|
+
@where_clauses = []
|
52
|
+
@join_clauses = []
|
53
|
+
end
|
54
|
+
|
55
|
+
def from(table)
|
56
|
+
@from_table_name = table
|
57
|
+
end
|
58
|
+
|
59
|
+
def select(columns)
|
60
|
+
@columns = map_column_value(columns)
|
61
|
+
end
|
62
|
+
|
63
|
+
def where(where_clause)
|
64
|
+
@where_clauses << where_clause
|
65
|
+
end
|
66
|
+
|
67
|
+
def join(join_clause)
|
68
|
+
@join_clauses << join_clause
|
69
|
+
end
|
70
|
+
|
71
|
+
private def map_column_value(columns)
|
72
|
+
columns.map do |c|
|
73
|
+
if c.raw_sql
|
74
|
+
QueryAst::ColumnValue::RawSql.new(name: c.name, value: c.raw_sql)
|
75
|
+
elsif c.replace_with
|
76
|
+
QueryAst::ColumnValue::ReplaceWith.new(name: c.name, value: c.replace_with)
|
77
|
+
else
|
78
|
+
QueryAst::ColumnValue::Plain.new(name: c.name, value: c.name)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exwiw
|
4
|
+
class QueryAstBuilder
|
5
|
+
def self.run(table_name, table_by_name, dump_target, logger)
|
6
|
+
new(table_name, table_by_name, dump_target, logger).run
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :table_name, :table_by_name, :dump_target
|
10
|
+
|
11
|
+
def initialize(table_name, table_by_name, dump_target, logger)
|
12
|
+
@table_name = table_name
|
13
|
+
@table_by_name = table_by_name
|
14
|
+
@dump_target = dump_target
|
15
|
+
@logger = logger
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
table = table_by_name.fetch(table_name)
|
20
|
+
|
21
|
+
where_clauses = build_where_clauses(table, dump_target)
|
22
|
+
join_clauses = build_join_clauses(table, table_by_name, dump_target)
|
23
|
+
|
24
|
+
QueryAst::Select.new.tap do |ast|
|
25
|
+
ast.from(table.name)
|
26
|
+
ast.select(table.columns)
|
27
|
+
join_clauses.each { |join_clause| ast.join(join_clause) }
|
28
|
+
where_clauses.each { |where_clause| ast.where(where_clause) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private def build_join_clauses(table, table_by_name, dump_target)
|
33
|
+
path_tables = find_path_to_dump_target(table, table_by_name, dump_target)
|
34
|
+
@logger.debug(" Join path from the table to dump target: #{path_tables}")
|
35
|
+
|
36
|
+
# the path is empty, it means that the table is not related to the dump target
|
37
|
+
# the path is 1, it's impossible case
|
38
|
+
return [] if path_tables.size < 2
|
39
|
+
|
40
|
+
join_clauses = []
|
41
|
+
|
42
|
+
path_tables.each_cons(2) do |from_table_name, to_table_name|
|
43
|
+
from_table = table_by_name[from_table_name]
|
44
|
+
to_table = table_by_name[to_table_name]
|
45
|
+
|
46
|
+
relation = from_table.belongs_to(to_table_name)
|
47
|
+
|
48
|
+
join_clause = QueryAst::JoinClause.new(
|
49
|
+
base_table_name: from_table.name,
|
50
|
+
foreign_key: relation.foreign_key,
|
51
|
+
join_table_name: to_table.name,
|
52
|
+
primary_key: to_table.primary_key,
|
53
|
+
where_clauses: []
|
54
|
+
)
|
55
|
+
relation_to_dump_target = to_table.belongs_to(dump_target.table_name)
|
56
|
+
if relation_to_dump_target
|
57
|
+
join_clause.where_clauses.push QueryAst::WhereClause.new(
|
58
|
+
column_name: relation_to_dump_target.foreign_key,
|
59
|
+
operator: :eq,
|
60
|
+
value: dump_target.ids
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
join_clauses.push(join_clause)
|
65
|
+
end
|
66
|
+
|
67
|
+
join_clauses
|
68
|
+
end
|
69
|
+
|
70
|
+
private def build_where_clauses(table, dump_target)
|
71
|
+
clauses = []
|
72
|
+
|
73
|
+
if table.name == dump_target.table_name
|
74
|
+
clauses.push Exwiw::QueryAst::WhereClause.new(
|
75
|
+
column_name: 'id',
|
76
|
+
operator: :eq,
|
77
|
+
value: dump_target.ids
|
78
|
+
)
|
79
|
+
|
80
|
+
return clauses
|
81
|
+
end
|
82
|
+
|
83
|
+
belongs_to = table.belongs_to(dump_target.table_name)
|
84
|
+
return clauses if belongs_to.nil?
|
85
|
+
|
86
|
+
clauses.push Exwiw::QueryAst::WhereClause.new(
|
87
|
+
column_name: belongs_to.foreign_key,
|
88
|
+
operator: :eq,
|
89
|
+
value: dump_target.ids
|
90
|
+
)
|
91
|
+
|
92
|
+
if table.filter
|
93
|
+
clauses.push table.filter
|
94
|
+
end
|
95
|
+
|
96
|
+
clauses
|
97
|
+
end
|
98
|
+
|
99
|
+
private def find_path_to_dump_target(table, table_by_name, dump_target)
|
100
|
+
return [] if table.name == dump_target.table_name
|
101
|
+
|
102
|
+
visited = {}
|
103
|
+
queue = [[table.name, []]]
|
104
|
+
|
105
|
+
until queue.empty?
|
106
|
+
current_table_name, path = queue.shift
|
107
|
+
current_table = table_by_name[current_table_name]
|
108
|
+
|
109
|
+
next if visited[current_table_name]
|
110
|
+
visited[current_table_name] = true
|
111
|
+
|
112
|
+
current_table.belongs_tos.each do |relation|
|
113
|
+
next_table_name = relation.table_name
|
114
|
+
next_path = path + [current_table_name]
|
115
|
+
|
116
|
+
return next_path if next_table_name == dump_target.table_name
|
117
|
+
|
118
|
+
queue.push([next_table_name, next_path])
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
queue
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|