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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exwiw
4
+ VERSION = "0.1.0"
5
+ 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: []