exwiw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|