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
data/lib/exwiw/runner.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module Exwiw
|
6
|
+
class Runner
|
7
|
+
def initialize(
|
8
|
+
connection_config:,
|
9
|
+
output_dir:,
|
10
|
+
config_dir:,
|
11
|
+
dump_target:,
|
12
|
+
logger:
|
13
|
+
)
|
14
|
+
@connection_config = connection_config
|
15
|
+
@output_dir = output_dir
|
16
|
+
@config_dir = config_dir
|
17
|
+
@dump_target = dump_target
|
18
|
+
@logger = logger
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
adapter = Adapter.build(@connection_config, @logger)
|
23
|
+
tables = load_table_config
|
24
|
+
|
25
|
+
@logger.info("Determining table processing order...")
|
26
|
+
ordered_table_names = DetermineTableProcessingOrder.run(tables)
|
27
|
+
|
28
|
+
if !Dir.exist?(@output_dir)
|
29
|
+
FileUtils.mkdir_p(@output_dir)
|
30
|
+
end
|
31
|
+
|
32
|
+
table_by_name = tables.each_with_object({}) { |table, hash| hash[table.name] = table }
|
33
|
+
|
34
|
+
total_size = ordered_table_names.size
|
35
|
+
ordered_table_names.each_with_index do |table_name, idx|
|
36
|
+
@logger.info("Processing table '#{table_name}'... (#{idx + 1}/#{total_size})")
|
37
|
+
table = table_by_name.fetch(table_name)
|
38
|
+
|
39
|
+
query_ast = QueryAstBuilder.run(table.name, table_by_name, @dump_target, @logger)
|
40
|
+
results = adapter.execute(query_ast)
|
41
|
+
record_num = results.size
|
42
|
+
|
43
|
+
if record_num.zero?
|
44
|
+
@logger.info(" No records matched. skip this table.")
|
45
|
+
next
|
46
|
+
end
|
47
|
+
@logger.debug(" Generate INSERT SQL...")
|
48
|
+
|
49
|
+
insert_sql = adapter.to_bulk_insert(results, table)
|
50
|
+
|
51
|
+
@logger.info(" Generated INSERT SQL for #{record_num} records.")
|
52
|
+
insert_idx = (idx + 1).to_s.rjust(3, '0')
|
53
|
+
File.open(File.join(@output_dir, "insert-#{insert_idx}-#{table_name}.sql"), 'w') do |file|
|
54
|
+
file.puts(insert_sql)
|
55
|
+
end
|
56
|
+
|
57
|
+
@logger.debug(" Generate DELETE SQL...")
|
58
|
+
delete_sql = adapter.to_bulk_delete(query_ast, table)
|
59
|
+
if @logger.debug?
|
60
|
+
@logger.debug(" Generated DELETE SQL:\n#{delete_sql}")
|
61
|
+
else
|
62
|
+
@logger.info(" Generated DELETE SQL.")
|
63
|
+
end
|
64
|
+
delete_idx = (total_size - idx).to_s.rjust(3, '0')
|
65
|
+
File.open(File.join(@output_dir, "delete-#{delete_idx}-#{table_name}.sql"), 'w') do |file|
|
66
|
+
file.puts(delete_sql)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private def load_table_config
|
72
|
+
Dir[File.join(@config_dir, "*.json")].map do |file|
|
73
|
+
json = JSON.parse(File.read(file))
|
74
|
+
TableConfig.from(json)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private def build_adapter
|
79
|
+
case @connection_config["adapter"]
|
80
|
+
when "sqlite3"
|
81
|
+
Sqlite3Adapter.new(@connection_config)
|
82
|
+
else
|
83
|
+
raise "Unsupported adapter"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exwiw
|
4
|
+
class TableColumn
|
5
|
+
include Serdes
|
6
|
+
|
7
|
+
attribute :name, String
|
8
|
+
attribute :replace_with, optional(String)
|
9
|
+
attribute :raw_sql, optional(String)
|
10
|
+
|
11
|
+
def self.from_symbol_keys(hash)
|
12
|
+
from(hash.transform_keys(&:to_s))
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_hash
|
16
|
+
super.compact # drop unusing option
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exwiw
|
4
|
+
class TableConfig
|
5
|
+
include Serdes
|
6
|
+
|
7
|
+
attribute :name, String
|
8
|
+
attribute :primary_key, String
|
9
|
+
attribute :filter, optional(String)
|
10
|
+
attribute :belongs_tos, array(BelongsTo)
|
11
|
+
attribute :columns, array(TableColumn)
|
12
|
+
|
13
|
+
def self.from_symbol_keys(hash)
|
14
|
+
from(hash.transform_keys(&:to_s))
|
15
|
+
end
|
16
|
+
|
17
|
+
def column_names
|
18
|
+
columns.map(&:name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def belongs_to(table_name)
|
22
|
+
belongs_tos.find { |relation| relation.table_name == table_name }
|
23
|
+
end
|
24
|
+
|
25
|
+
def build_extract_query(extract_target_table, extract_target_ids, tables_by_name)
|
26
|
+
# target is itself
|
27
|
+
if name == extract_target_table
|
28
|
+
return [{
|
29
|
+
from: name,
|
30
|
+
where: [{ primary_key => extract_target_ids }],
|
31
|
+
join: [],
|
32
|
+
select: column_names,
|
33
|
+
}]
|
34
|
+
end
|
35
|
+
|
36
|
+
# it is not related to target table
|
37
|
+
if belongs_to.empty?
|
38
|
+
return [{
|
39
|
+
from: name,
|
40
|
+
where: [],
|
41
|
+
join: [],
|
42
|
+
select: column_names,
|
43
|
+
}]
|
44
|
+
end
|
45
|
+
|
46
|
+
belongs_to_extract_target_table = belongs_tos.find { |relation| relation.table_name == extract_target_table }
|
47
|
+
if belongs_to_extract_target_table
|
48
|
+
key = belongs_to_extract_target_table.foreign_key
|
49
|
+
return [{ from: name, where: [{ key => extract_target_ids }], join: [], select: column_names }]
|
50
|
+
end
|
51
|
+
|
52
|
+
ret = compute_dependency_to_table(extract_target_table, tables_by_name)
|
53
|
+
|
54
|
+
if ret.empty?
|
55
|
+
[{
|
56
|
+
from: name,
|
57
|
+
where: [],
|
58
|
+
join: [],
|
59
|
+
select: column_names,
|
60
|
+
}]
|
61
|
+
else
|
62
|
+
last = ret.last
|
63
|
+
last[:where] = [{ last[:foreign_key] => extract_target_ids }]
|
64
|
+
ret
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private def compute_dependency_to_table(target_table_name, tables_by_name)
|
69
|
+
return [] if belongs_tos.empty?
|
70
|
+
|
71
|
+
results = belongs_tos.map do |relation|
|
72
|
+
relation_table = tables_by_name[relation.table_name]
|
73
|
+
|
74
|
+
if relation_table.name == target_table_name
|
75
|
+
[{ base_table_name: name, foreign_key: relation.foreign_key,
|
76
|
+
join_table_name: target_table_name, join_key: relation_table.primary_key }]
|
77
|
+
else
|
78
|
+
ret = relation_table.compute_dependency_to_table(target_table_name, tables_by_name)
|
79
|
+
[{ base_table_name: name, foreign_key: relation.foreign_key,
|
80
|
+
join_table_name: relation_table.name, join_key: relation_table.primary_key }] + ret
|
81
|
+
end
|
82
|
+
end.compact
|
83
|
+
|
84
|
+
matched_dependencies = results.select do |dependency|
|
85
|
+
dependency.last[:join_table_name] == target_table_name
|
86
|
+
end
|
87
|
+
|
88
|
+
return [] if matched_dependencies.empty?
|
89
|
+
|
90
|
+
matched_dependencies.min_by(&:size)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/exwiw.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "exwiw/version"
|
4
|
+
|
5
|
+
require "json"
|
6
|
+
require "serdes"
|
7
|
+
|
8
|
+
require_relative "exwiw/adapter"
|
9
|
+
require_relative "exwiw/adapter/sqlite3_adapter"
|
10
|
+
require_relative "exwiw/adapter/mysql2_adapter"
|
11
|
+
require_relative "exwiw/adapter/postgresql_adapter"
|
12
|
+
require_relative "exwiw/determine_table_processing_order"
|
13
|
+
require_relative "exwiw/query_ast"
|
14
|
+
require_relative "exwiw/query_ast_builder"
|
15
|
+
require_relative "exwiw/runner"
|
16
|
+
require_relative "exwiw/belongs_to"
|
17
|
+
require_relative "exwiw/table_column"
|
18
|
+
require_relative "exwiw/table_config"
|
19
|
+
|
20
|
+
begin
|
21
|
+
require 'rails'
|
22
|
+
rescue LoadError
|
23
|
+
else
|
24
|
+
require 'exwiw/railtie'
|
25
|
+
end
|
26
|
+
|
27
|
+
module Exwiw
|
28
|
+
DumpTarget = Struct.new(:table_name, :ids, keyword_init: true)
|
29
|
+
ConnectionConfig = Struct.new(:adapter, :host, :port, :user, :password, :database_name, keyword_init: true)
|
30
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :exwiw do
|
4
|
+
namespace :schema do
|
5
|
+
desc "Generate schema from application"
|
6
|
+
task generate: :environment do
|
7
|
+
require "json"
|
8
|
+
require "exwiw"
|
9
|
+
require "fileutils"
|
10
|
+
|
11
|
+
Rails.application.eager_load!
|
12
|
+
|
13
|
+
table_by_name = {}
|
14
|
+
|
15
|
+
ActiveRecord::Base.descendants.each do |model|
|
16
|
+
next if model.abstract_class?
|
17
|
+
next if table_by_name[model.table_name]
|
18
|
+
|
19
|
+
belongs_tos = model.reflect_on_all_associations(:belongs_to).map do |assoc|
|
20
|
+
if assoc.polymorphic?
|
21
|
+
# XXX: Support polymorphic
|
22
|
+
next
|
23
|
+
else
|
24
|
+
Exwiw::BelongsTo.from_symbol_keys({
|
25
|
+
table_name: assoc.table_name,
|
26
|
+
foreign_key: assoc.foreign_key,
|
27
|
+
})
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
columns = model.column_names.map do |name|
|
32
|
+
Exwiw::TableColumn.from_symbol_keys({ name: name })
|
33
|
+
end
|
34
|
+
|
35
|
+
table = Exwiw::TableConfig.from_symbol_keys({
|
36
|
+
name: model.table_name,
|
37
|
+
primary_key: model.primary_key,
|
38
|
+
belongs_tos: belongs_tos.compact,
|
39
|
+
columns: columns,
|
40
|
+
})
|
41
|
+
table_by_name[table.name] = table
|
42
|
+
end
|
43
|
+
|
44
|
+
tables = table_by_name.values
|
45
|
+
|
46
|
+
FileUtils.mkdir_p("exwiw")
|
47
|
+
|
48
|
+
tables.each do |table|
|
49
|
+
path = File.join("exwiw", "#{table.name}.json")
|
50
|
+
File.write(path, JSON.pretty_generate(table.to_hash))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: exwiw
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shia
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-02-17 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: serdes
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0.1'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0.1'
|
26
|
+
description: Export What I Want (Exwiw) is a Ruby gem that allows you to export records
|
27
|
+
from a database to a dump file.
|
28
|
+
email:
|
29
|
+
- rise.shia@gmail.com
|
30
|
+
executables:
|
31
|
+
- exwiw
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- CHANGELOG.md
|
36
|
+
- LICENSE.txt
|
37
|
+
- README.md
|
38
|
+
- exe/exwiw
|
39
|
+
- lib/exwiw.rb
|
40
|
+
- lib/exwiw/adapter.rb
|
41
|
+
- lib/exwiw/adapter/mysql2_adapter.rb
|
42
|
+
- lib/exwiw/adapter/postgresql_adapter.rb
|
43
|
+
- lib/exwiw/adapter/sqlite3_adapter.rb
|
44
|
+
- lib/exwiw/belongs_to.rb
|
45
|
+
- lib/exwiw/cli.rb
|
46
|
+
- lib/exwiw/determine_table_processing_order.rb
|
47
|
+
- lib/exwiw/query_ast.rb
|
48
|
+
- lib/exwiw/query_ast_builder.rb
|
49
|
+
- lib/exwiw/railtie.rb
|
50
|
+
- lib/exwiw/runner.rb
|
51
|
+
- lib/exwiw/table_column.rb
|
52
|
+
- lib/exwiw/table_config.rb
|
53
|
+
- lib/exwiw/version.rb
|
54
|
+
- lib/tasks/exwiw.rake
|
55
|
+
homepage: https://github.com/riseshia/exwiw
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
metadata:
|
59
|
+
homepage_uri: https://github.com/riseshia/exwiw
|
60
|
+
source_code_uri: https://github.com/riseshia/exwiw/tree/main
|
61
|
+
changelog_uri: https://github.com/riseshia/exwiw/blob/main/CHANGELOG.md
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 3.2.0
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements: []
|
76
|
+
rubygems_version: 3.6.2
|
77
|
+
specification_version: 4
|
78
|
+
summary: Ruby gem that allows you to export records from a database to a dump file.
|
79
|
+
test_files: []
|