gamma 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 257847b37232eb4878c3a62a96994213ea5d8fa6
4
+ data.tar.gz: 8d856eac239f3646c212473b5b1dd57349c3c2b8
5
+ SHA512:
6
+ metadata.gz: e624da76ffada8e2b46d724d33fb778ec7d55adb25afeb491c15dd3c8fa22aca140faa6819d3678097eb5c23c5f9c562dce99599600eeb4722318e804bd8cf14
7
+ data.tar.gz: 55aa8bc74a6fdca5c17095fb5ad5435ccf732fbc18b38b871df2b8a34ebd26396e3630d8e4df7c6f06015d6c234c6627ab7022d5d7ec6782bac3468838fee3c8
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ .DS_Store
11
+ history.json
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.2
5
+ before_install: gem install bundler -v 1.16.0
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at nishio@densan-labs.net. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in gamma.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,46 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ gamma (0.1.0)
5
+ activesupport
6
+ colorize (~> 0.8.1)
7
+ mysql2
8
+ thor (~> 0.20)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ activesupport (5.1.5)
14
+ concurrent-ruby (~> 1.0, >= 1.0.2)
15
+ i18n (~> 0.7)
16
+ minitest (~> 5.1)
17
+ tzinfo (~> 1.1)
18
+ coderay (1.1.2)
19
+ colorize (0.8.1)
20
+ concurrent-ruby (1.0.5)
21
+ i18n (0.9.5)
22
+ concurrent-ruby (~> 1.0)
23
+ method_source (0.9.0)
24
+ minitest (5.11.3)
25
+ mysql2 (0.4.10)
26
+ pry (0.11.3)
27
+ coderay (~> 1.1.0)
28
+ method_source (~> 0.9.0)
29
+ rake (10.5.0)
30
+ thor (0.20.0)
31
+ thread_safe (0.3.6)
32
+ tzinfo (1.2.5)
33
+ thread_safe (~> 0.1)
34
+
35
+ PLATFORMS
36
+ ruby
37
+
38
+ DEPENDENCIES
39
+ bundler (~> 1.16)
40
+ gamma!
41
+ minitest (~> 5.0)
42
+ pry
43
+ rake (~> 10.0)
44
+
45
+ BUNDLED WITH
46
+ 1.16.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Shinsuke Nishio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # Gamma
2
+
3
+ Database Synchronizer. Transfer data from one database to another easily.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'gamma'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install gamma
20
+
21
+ ## Usage
22
+
23
+ ### Dryrun
24
+
25
+ ```
26
+ gamma dryrun --settings ./tmp/settings.yml --data ./tmp/data.yml
27
+ ```
28
+
29
+ ### Apply
30
+
31
+ ```
32
+ gamma apply --settings ./tmp/settings.yml --data ./tmp/data.yml
33
+ ```
34
+
35
+ ### data.yml Example
36
+
37
+ ```
38
+ - data:
39
+ table:
40
+ - "*"
41
+ table_without:
42
+ - "users"
43
+ - "schema_migrations"
44
+ - "ar_internal_metadata"
45
+ mode: "replace"
46
+ delta_column: "updated_at"
47
+ - data:
48
+ table: "users"
49
+ mode: "replace"
50
+ delta_column: "updated_at"
51
+ hooks:
52
+ - column:
53
+ name:
54
+ - "email"
55
+ scripts:
56
+ - "hooks/mask_email.rb"
57
+ - column:
58
+ name:
59
+ - "name"
60
+ scripts:
61
+ - "hooks/mask_name.rb"
62
+ - row:
63
+ scripts:
64
+ - "hooks/image.rb"
65
+ ```
66
+
67
+ ### Hook Script Example
68
+
69
+ ```
70
+ # $YourDir/hooks/mask_name.rb
71
+
72
+ class MaskName
73
+ def execute(apply, column, value)
74
+ value = "●●#{value[2..-1]}"
75
+ value
76
+ end
77
+ end
78
+
79
+ # $YourDir/hooks/image.rb
80
+
81
+ class Image
82
+ def execute(apply, record)
83
+ #
84
+ # Copy image data from one storage to another
85
+ #
86
+
87
+ record
88
+ end
89
+ end
90
+ ```
91
+
92
+ ### settings.yml Example
93
+
94
+ ```
95
+ in_database_config:
96
+ adapter: mysql2
97
+ encoding: utf8
98
+ database: real_database
99
+ pool: 5
100
+ host: localhost
101
+ username: root
102
+ password:
103
+ out_database_config:
104
+ adapter: mysql2
105
+ encoding: utf8
106
+ database: sync_to_database
107
+ pool: 5
108
+ host: localhost
109
+ username: root
110
+ password:
111
+ ```
112
+
113
+ ## Development
114
+
115
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
116
+
117
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
118
+
119
+ ## Contributing
120
+
121
+ Bug reports and pull requests are welcome on GitHub at https://github.com/nishio-dens/gamma. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
122
+
123
+ ## License
124
+
125
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
126
+
127
+ ## Code of Conduct
128
+
129
+ Everyone interacting in the Gamma project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nishio-dens/gamma/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "gamma"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/gamma ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
3
+ require "rubygems"
4
+ require "gamma"
5
+ require "thor"
6
+
7
+ class GammaCLI < Thor
8
+
9
+ desc "apply", "Apply"
10
+ option :settings, aliases: "-s", desc: "Database Settings yaml", required: true
11
+ option :data, aliases: "-d", desc: "Table Sync Settings yaml", required: true
12
+ option :hook_dir, aliases: "-h", desc: "Hook script directory"
13
+ option :sync_history, desc: "Hook script directory"
14
+ def apply
15
+ Gamma::Command::Apply.new(options).execute
16
+ end
17
+
18
+ desc "dryrun", "Dryrun"
19
+ option :settings, aliases: "-s", desc: "Database Settings yaml", required: true
20
+ option :data, aliases: "-d", desc: "Table Sync Settings yaml", required: true
21
+ option :hook_dir, aliases: "-h", desc: "Hook script directory"
22
+ option :sync_history, desc: "Hook script directory"
23
+ def dryrun
24
+ Gamma::Command::Dryrun.new(options).execute
25
+ end
26
+ end
27
+
28
+ GammaCLI.start(ARGV)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/gamma.gemspec ADDED
@@ -0,0 +1,35 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "gamma/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "gamma"
8
+ spec.version = Gamma::VERSION
9
+ spec.authors = ["Shinsuke Nishio"]
10
+ spec.email = ["nishio@densan-labs.net"]
11
+
12
+ spec.summary = %q{DBSync}
13
+ spec.description = %q{DBSync}
14
+ spec.homepage = "https://github.com/nishio-dens/gamma"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "mysql2"
25
+ spec.add_dependency "activesupport"
26
+ spec.add_dependency "thor", "~> 0.20"
27
+ spec.add_dependency "colorize", "~> 0.8.1"
28
+
29
+ spec.required_ruby_version = ">= 2.3.0"
30
+
31
+ spec.add_development_dependency "bundler", "~> 1.16"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "minitest", "~> 5.0"
34
+ spec.add_development_dependency "pry"
35
+ end
data/lib/gamma.rb ADDED
@@ -0,0 +1,31 @@
1
+ require "rubygems"
2
+ require "active_support"
3
+ require "active_support/core_ext"
4
+ require "colorize"
5
+ require "logger"
6
+ require "json"
7
+
8
+ if ENV["DEBUG"].present?
9
+ require "pry"
10
+ end
11
+
12
+ require "gamma/version"
13
+ require "gamma/table"
14
+ require "gamma/hook"
15
+ require "gamma/script"
16
+ require "gamma/script/row_script"
17
+ require "gamma/script/column_script"
18
+ require "gamma/sync_database"
19
+ require "gamma/database_settings"
20
+ require "gamma/database_connector"
21
+ require "gamma/database_connector/mysql_connector"
22
+ require "gamma/parser"
23
+ require "gamma/parser/data_parser"
24
+ require "gamma/importer"
25
+ require "gamma/importer/replace"
26
+ require "gamma/command"
27
+ require "gamma/command/apply"
28
+ require "gamma/command/dryrun"
29
+
30
+ module Gamma
31
+ end
@@ -0,0 +1,16 @@
1
+ class Gamma::Command
2
+ def gamma_tables(in_client, out_client, data_parser)
3
+ database_exist_tables = database_tables(in_client, out_client, data_parser)
4
+ end
5
+
6
+ def output_setting_warning(tables)
7
+ dup_tables = tables.group_by { |t| t.table_name }.select { |k, v| v.size > 1 }.map(&:first)
8
+ dup_tables.each do |tname|
9
+ logger.warn("Table *#{tname}* settings are duplicated. Please review your data settings.".red)
10
+ end
11
+ end
12
+
13
+ def logger
14
+ @_logger ||= Logger.new(STDOUT)
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ class Gamma::Command::Apply < Gamma::Command
2
+ def initialize(opts)
3
+ @database_settings = Gamma::DatabaseSettings.new(opts[:settings])
4
+ # TODO: support postgres adapter
5
+ @in_client = Gamma::DatabaseConnector::MysqlConnector.new(@database_settings.in_database)
6
+ @out_client = Gamma::DatabaseConnector::MysqlConnector.new(@database_settings.out_database)
7
+ @hook_root_dir = opts[:hook_dir] || "."
8
+ @syncdb = Gamma::SyncDatabase.new(opts[:sync_history] || "./history.json")
9
+ @data_parser = Gamma::Parser::DataParser.new(opts[:data], @hook_root_dir, @in_client, @out_client, apply: true)
10
+ end
11
+
12
+ def execute
13
+ tables = @data_parser.gamma_tables
14
+ output_setting_warning(tables)
15
+
16
+ tables.each do |t|
17
+ logger.info("[#{t.sync_mode}] Sync Start #{t.table_name}".green)
18
+
19
+ case t.sync_mode
20
+ when "replace"
21
+ Gamma::Importer::Replace.new(@in_client, @out_client, t, apply: true).execute
22
+ else
23
+ logger.info("[#{t.sync_mode}] Sync Failed #{t.table_name}. Unknown Sync mode".red)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ class Gamma::Command::Dryrun < Gamma::Command
2
+ def initialize(opts)
3
+ @database_settings = Gamma::DatabaseSettings.new(opts[:settings])
4
+ # TODO: support postgres adapter
5
+ @in_client = Gamma::DatabaseConnector::MysqlConnector.new(@database_settings.in_database)
6
+ @out_client = Gamma::DatabaseConnector::MysqlConnector.new(@database_settings.out_database)
7
+ @hook_root_dir = opts[:hook_dir] || "."
8
+ @syncdb = Gamma::SyncDatabase.new(opts[:sync_history] || "./history.json")
9
+ @data_parser = Gamma::Parser::DataParser.new(opts[:data], @hook_root_dir, @in_client, @out_client, apply: false)
10
+ end
11
+
12
+ def execute
13
+ tables = @data_parser.gamma_tables
14
+ output_setting_warning(tables)
15
+
16
+ tables.each do |t|
17
+ logger.info("[#{t.sync_mode}] Sync Start #{t.table_name}".green)
18
+
19
+ case t.sync_mode
20
+ when "replace"
21
+ Gamma::Importer::Replace.new(@in_client, @out_client, t).execute
22
+ else
23
+ logger.info("[#{t.sync_mode}] Sync Failed #{t.table_name}. Unknown Sync mode".red)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,2 @@
1
+ class Gamma::DatabaseConnector
2
+ end
@@ -0,0 +1,31 @@
1
+ require 'mysql2'
2
+
3
+ class Gamma::DatabaseConnector::MysqlConnector < Gamma::DatabaseConnector
4
+ DEFAULT_PORT = 3306
5
+
6
+ attr_reader :config
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ def client(database_name = @config[:database])
13
+ @_client ||= Mysql2::Client.new(
14
+ host: @config[:host],
15
+ port: @config[:port] || DEFAULT_PORT,
16
+ username: @config[:username],
17
+ password: @config[:password] || "",
18
+ database: database_name
19
+ )
20
+ end
21
+
22
+ def schema_client
23
+ @_schema_client ||= Mysql2::Client.new(
24
+ host: @config[:host],
25
+ port: @config[:port] || DEFAULT_PORT,
26
+ username: @config[:username],
27
+ password: @config[:password] || "",
28
+ database: "information_schema"
29
+ )
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ class Gamma::DatabaseSettings
2
+ attr_reader :settings, :in_database, :out_database
3
+
4
+ def initialize(yaml_path)
5
+ @settings = YAML.load_file(yaml_path).with_indifferent_access
6
+ @in_database = @settings[:in_database_config]
7
+ @out_database = @settings[:out_database_config]
8
+ end
9
+ end
data/lib/gamma/hook.rb ADDED
@@ -0,0 +1,30 @@
1
+ class Gamma::Hook
2
+ attr_accessor :hook_type, :column_name, :script_path, :root_dir, :apply
3
+
4
+ def execute_script(record)
5
+ path = File.join(root_dir, script_path)
6
+ fail "Hook Scripts Not Found. path: #{path}" unless File.exist?(path)
7
+
8
+ result = record
9
+ scripts = open(File.join(root_dir, script_path)).read
10
+ eval(scripts)
11
+
12
+ begin
13
+ klass_name = "Gamma::Hook::#{File.basename(path, ".*").camelize}"
14
+ instance = klass_name.constantize.new
15
+ case self.hook_type.to_s
16
+ when "column"
17
+ r = instance.execute(apply, column_name.to_s, record[column_name.to_s])
18
+ record[column_name.to_s] = r
19
+ when "row"
20
+ record = instance.execute(apply, record)
21
+ else
22
+ fail "Error"
23
+ end
24
+ rescue => e
25
+ raise "Invalid Hook Class #{klass_name}"
26
+ end
27
+
28
+ result
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ class Gamma::Importer
2
+ def logger
3
+ @_logger ||= Logger.new(STDOUT)
4
+ end
5
+
6
+ def set_foreign_key_off(client)
7
+ client.client.query("SET FOREIGN_KEY_CHECKS=0")
8
+ end
9
+
10
+ def set_foreign_key_on(client)
11
+ client.client.query("SET FOREIGN_KEY_CHECKS=1")
12
+ end
13
+ end
@@ -0,0 +1,124 @@
1
+ class Gamma::Importer::Replace < Gamma::Importer
2
+ BATCH_SIZE = 1000
3
+
4
+ def initialize(in_client, out_client, table, apply: false)
5
+ @in_client = in_client
6
+ @out_client = out_client
7
+ @table = table
8
+ @apply = apply
9
+ end
10
+
11
+ def execute
12
+ set_foreign_key_off(@out_client)
13
+ batch_sync
14
+ ensure
15
+ set_foreign_key_on(@out_client)
16
+ end
17
+
18
+ private
19
+
20
+ def batch_sync
21
+ columns = @table.in_exist_columns & @table.out_exist_columns
22
+ primary_key = "id" # TODO: Fixme
23
+
24
+ current_in_pid = 0
25
+ current_in_offset = 0
26
+ while true do
27
+ select_columns = columns.map { |c| "`#{c}`" }.join(",")
28
+ break unless select_columns.present?
29
+
30
+ in_query = "SELECT #{select_columns} FROM #{@table.table_name} WHERE id > #{current_in_pid} LIMIT #{BATCH_SIZE}"
31
+ logger.info(in_query) if ENV["DEBUG"]
32
+ in_records = @in_client.client.query(in_query).to_a
33
+
34
+ break unless in_records.present?
35
+
36
+ out_records = exist_records(select_columns, primary_key, in_records.map { |v| v[primary_key] })
37
+
38
+ in_records.map { |ir| [ir, out_records.find { |v| ir[primary_key] == v[primary_key] }] }.each do |in_record, out_record|
39
+ if out_record.present?
40
+ update_out_record(in_record, out_record, primary_key)
41
+ else
42
+ insert_out_record(in_record)
43
+ end
44
+ end
45
+
46
+ current_in_pid = in_records.last[primary_key]
47
+ current_in_offset = current_in_offset + BATCH_SIZE
48
+ end
49
+ rescue => e
50
+ logger.error("Sync Error #{@table.table_name} \n #{e}\n #{e.backtrace.join("\n")}".red)
51
+ end
52
+
53
+ def exist_records(select_columns, primary_key, in_pids)
54
+ return [] unless in_pids.present?
55
+
56
+ query = "SELECT #{select_columns} FROM #{@table.table_name} WHERE #{primary_key} in (#{in_pids.join(",")})"
57
+ @out_client.client.query(query).to_a
58
+ end
59
+
60
+ def update_out_record(in_record, out_record, primary_key)
61
+ need_update = @table.delta_column.blank?
62
+ need_update ||= @table.delta_column.present? && in_record[@table.delta_column] != out_record[@table.delta_column]
63
+ if need_update
64
+ record = @table.record_value(in_record)
65
+ columns = (@table.in_exist_columns & @table.out_exist_columns).reject { |c| record[c].nil? }
66
+ values = update_record_values(record, columns)
67
+
68
+ query = <<-EOS
69
+ UPDATE `#{@table.table_name}` SET #{values} WHERE #{primary_key} = #{record[primary_key]}
70
+ EOS
71
+ query = query.strip_heredoc
72
+ logger.info(query) if ENV["DEBUG"]
73
+
74
+ if @apply
75
+ @out_client.client.query(query)
76
+ else
77
+ logger.info("DRYRUN: #{query}")
78
+ end
79
+ end
80
+ end
81
+
82
+ def insert_out_record(in_record)
83
+ record = @table.record_value(in_record)
84
+ columns = (@table.in_exist_columns & @table.out_exist_columns).reject { |c| record[c].nil? }
85
+ select_columns = columns.map { |c| "`#{c}`" }.join(",")
86
+ values = insert_record_values(record, columns)
87
+
88
+ query = <<-EOS
89
+ INSERT INTO #{@table.table_name}(#{select_columns}) VALUES (#{values})
90
+ EOS
91
+ query = query.strip_heredoc
92
+ logger.info(query) if ENV["DEBUG"]
93
+
94
+ if @apply
95
+ @out_client.client.query(query)
96
+ else
97
+ logger.info("DRYRUN: #{query}")
98
+ end
99
+ end
100
+
101
+ def insert_record_values(record, columns)
102
+ r = record
103
+ columns.map do |v|
104
+ c = if r[v].is_a?(Time)
105
+ r[v].strftime("%Y-%m-%d %H:%M:%S")
106
+ else
107
+ r[v]
108
+ end
109
+ "\"#{c.to_s.gsub('"', '\"')}\""
110
+ end.join(",")
111
+ end
112
+
113
+ def update_record_values(record, columns)
114
+ r = record
115
+ columns.map do |v|
116
+ c = if r[v].is_a?(Time)
117
+ r[v].strftime("%Y-%m-%d %H:%M:%S")
118
+ else
119
+ r[v]
120
+ end
121
+ "#{v} = \"#{c}\""
122
+ end.join(",")
123
+ end
124
+ end
@@ -0,0 +1,2 @@
1
+ class Gamma::Parser
2
+ end
@@ -0,0 +1,131 @@
1
+ class Gamma::Parser::DataParser < Gamma::Parser
2
+ DEFAULT_SYNC_MODE = "replace"
3
+
4
+ def initialize(data_yaml_path, hook_root_dir, in_client, out_client, apply: false)
5
+ @data_settings = YAML.load_file(data_yaml_path).map(&:with_indifferent_access)
6
+ @hook_root_dir = hook_root_dir
7
+ @in_client = in_client
8
+ @out_client = out_client
9
+ @apply = apply
10
+ end
11
+
12
+ def gamma_tables
13
+ exist_tables = database_exist_tables
14
+ @data_settings.map do |d|
15
+ parse_data_settings(d[:data], exist_tables)
16
+ end.flatten
17
+ end
18
+
19
+ def parse_data_settings(data, exist_tables)
20
+ tables = if Array(data[:table]).join == "*"
21
+ without = Array(data[:table_without]) || []
22
+ exist_tables.reject { |v| without.include?(v.table_name) }
23
+ else
24
+ Array(data[:table]).map do |table_name|
25
+ exist_tables.find { |t| t.table_name == table_name }
26
+ end.compact
27
+ end
28
+ tables = tables.map do |t|
29
+ t.sync_mode = data[:mode].presence || DEFAULT_SYNC_MODE
30
+ t.delta_column = data[:delta_column]
31
+ t.hooks = data[:hooks].present? ? parse_hooks(data[:hooks], t) : []
32
+
33
+ t
34
+ end
35
+ tables
36
+ end
37
+
38
+ private
39
+
40
+ def database_exist_tables
41
+ in_tables = select_table_definitions(@in_client)
42
+ out_tables = select_table_definitions(@out_client)
43
+
44
+ (in_tables + out_tables).uniq.map do |table|
45
+ t = Gamma::Table.new
46
+ t.table_name = table
47
+ t.in_exist = in_tables.include?(table)
48
+ t.out_exist = out_tables.include?(table)
49
+ t.in_exist_columns = select_column_definitions(@in_client, table)
50
+ t.out_exist_columns = select_column_definitions(@out_client, table)
51
+ t
52
+ end
53
+ end
54
+
55
+ def select_table_definitions(client)
56
+ query = <<-EOS
57
+ SELECT
58
+ *
59
+ FROM
60
+ TABLES
61
+ INNER JOIN
62
+ COLLATION_CHARACTER_SET_APPLICABILITY CCSA
63
+ ON
64
+ TABLES.TABLE_COLLATION = CCSA.COLLATION_NAME
65
+ WHERE
66
+ TABLE_SCHEMA = '#{client.schema_client.escape(client.config[:database])}'
67
+ ORDER BY
68
+ TABLE_NAME
69
+ EOS
70
+ client.schema_client.query(query.strip_heredoc).to_a.map { |v| v["TABLE_NAME"] }
71
+ end
72
+
73
+ def select_column_definitions(client, table_name)
74
+ query = <<-EOS
75
+ SELECT
76
+ *
77
+ FROM
78
+ COLUMNS
79
+ WHERE
80
+ TABLE_SCHEMA = '#{client.schema_client.escape(client.config[:database])}'
81
+ AND TABLE_NAME = '#{client.schema_client.escape(table_name)}'
82
+ ORDER BY
83
+ TABLE_NAME, ORDINAL_POSITION
84
+ EOS
85
+ client.schema_client.query(query.strip_heredoc).to_a.map { |v| v["COLUMN_NAME"] }
86
+ end
87
+
88
+ def parse_hooks(hooks, table)
89
+ hooks = hooks.is_a?(Array) ? hooks : [hooks]
90
+ hooks.map do |hook|
91
+ type = if hook[:row].present?
92
+ :row
93
+ elsif hook[:column].present?
94
+ :column
95
+ end
96
+
97
+ if type == :row
98
+ options = hook[:row]
99
+ fail "Required scripts arguments. table: #{table.table_name}, hook_type: #{type}" unless options[:scripts].present?
100
+
101
+ Array(options[:scripts]).map do |script|
102
+ h = Gamma::Hook.new
103
+ h.hook_type = :row
104
+ h.column_name = nil
105
+ h.script_path = script
106
+ h.root_dir = @hook_root_dir
107
+ h.apply = @apply
108
+ h
109
+ end
110
+ elsif type == :column
111
+ options = hook[:column]
112
+ fail "Required column name arguments. table: #{table.table_name}, hook_type: #{type}" unless options[:name].present?
113
+ fail "Required scripts arguments. table: #{table.table_name}, hook_type: #{type}" unless options[:scripts].present?
114
+
115
+ column_names = Array(options[:name])
116
+ scripts = Array(options[:scripts])
117
+ column_names.product(scripts).map do |column_name, script|
118
+ h = Gamma::Hook.new
119
+ h.hook_type = :column
120
+ h.column_name = column_name
121
+ h.script_path = script
122
+ h.root_dir = @hook_root_dir
123
+ h.apply = @apply
124
+ h
125
+ end
126
+ else
127
+ fail "Unknown Hook Type"
128
+ end
129
+ end.flatten.compact
130
+ end
131
+ end
@@ -0,0 +1,2 @@
1
+ class Gamma::Script
2
+ end
@@ -0,0 +1,5 @@
1
+ class Gamma::Script::ColumnScript
2
+ def execute(apply, column, value)
3
+ fail "Not Implemented"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Gamma::Script::RowScript
2
+ def execute(apply, value)
3
+ fail "Not Implemented"
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ class Gamma::SyncDatabase
2
+ def initialize(path)
3
+ @sync_history_path = path
4
+ @database = if File.exists?(@sync_history_path)
5
+ JSON.parse(open(@sync_history_path).read)
6
+ else
7
+ {}
8
+ end
9
+ end
10
+
11
+ def save
12
+ open(@sync_history_path, "w") do |io|
13
+ JSON.dump(@database, io)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ class Gamma::Table
2
+ attr_accessor :table_name, :in_exist, :out_exist, :in_exist_columns, :out_exist_columns
3
+ attr_accessor :sync_mode, :delta_column
4
+ attr_accessor :hooks
5
+
6
+ def record_value(record)
7
+ row_hooks = hooks.select { |h| h.hook_type.to_s == "row" }
8
+ column_hooks = hooks.select { |h| h.hook_type.to_s == "column" }
9
+
10
+ result = record
11
+ result = row_hooks.reduce(record) { |h, rec| execute_row_hook(rec, h) } if row_hooks.present?
12
+ result = column_hooks.reduce(record) { |h, rec| execute_row_hook(rec, h) } if column_hooks.present?
13
+
14
+ result
15
+ end
16
+
17
+ private
18
+
19
+ def execute_row_hook(hook, record)
20
+ hook.execute_script(record)
21
+ end
22
+
23
+ def execute_column_hook(hook, record)
24
+ record
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module Gamma
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,186 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gamma
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shinsuke Nishio
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-03-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mysql2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.20'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.20'
55
+ - !ruby/object:Gem::Dependency
56
+ name: colorize
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.8.1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.8.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.16'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.16'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: minitest
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '5.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '5.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: DBSync
126
+ email:
127
+ - nishio@densan-labs.net
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - ".travis.yml"
134
+ - CODE_OF_CONDUCT.md
135
+ - Gemfile
136
+ - Gemfile.lock
137
+ - LICENSE.txt
138
+ - README.md
139
+ - Rakefile
140
+ - bin/console
141
+ - bin/gamma
142
+ - bin/setup
143
+ - gamma.gemspec
144
+ - lib/gamma.rb
145
+ - lib/gamma/command.rb
146
+ - lib/gamma/command/apply.rb
147
+ - lib/gamma/command/dryrun.rb
148
+ - lib/gamma/database_connector.rb
149
+ - lib/gamma/database_connector/mysql_connector.rb
150
+ - lib/gamma/database_settings.rb
151
+ - lib/gamma/hook.rb
152
+ - lib/gamma/importer.rb
153
+ - lib/gamma/importer/replace.rb
154
+ - lib/gamma/parser.rb
155
+ - lib/gamma/parser/data_parser.rb
156
+ - lib/gamma/script.rb
157
+ - lib/gamma/script/column_script.rb
158
+ - lib/gamma/script/row_script.rb
159
+ - lib/gamma/sync_database.rb
160
+ - lib/gamma/table.rb
161
+ - lib/gamma/version.rb
162
+ homepage: https://github.com/nishio-dens/gamma
163
+ licenses:
164
+ - MIT
165
+ metadata: {}
166
+ post_install_message:
167
+ rdoc_options: []
168
+ require_paths:
169
+ - lib
170
+ required_ruby_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: 2.3.0
175
+ required_rubygems_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ requirements: []
181
+ rubyforge_project:
182
+ rubygems_version: 2.6.13
183
+ signing_key:
184
+ specification_version: 4
185
+ summary: DBSync
186
+ test_files: []