breathing 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 815ec172eb186702177838e5213fb84c471b15dc7a4bdd7c951a97c78d9666c2
4
- data.tar.gz: d05a272b946c324ca9f0031e291840ca58d21069607ffc2e8b0579e9a99ecb76
3
+ metadata.gz: 62878c327e1da216456749f0d3d3b154a4d8d11528226f90cc7dadf855d2e6f2
4
+ data.tar.gz: 7a27d00aca80c0a3837d1bfc7778ee1d507c2deb7719127a53745a86f25aa439
5
5
  SHA512:
6
- metadata.gz: f5130a4714437d96c658266626989a86feb1c670d62308168dc64b009a0ea660aa86d8ede410acba4fdd6bb0a33504248f0c6ecca1e9b95ecc74293783ea03c5
7
- data.tar.gz: 1c624939e64701ab5dc42bcbac6ac46cfae7ece8dd967ee1d960c6f1a209b63c42bdaf0d454e50a8bd06d1fe29982967c85098af683da1b0e93c7c3a85a197e0
6
+ metadata.gz: 86f14f92c0a6b4953307c6a733daf0c378726f0e3d9f97703c29ff3e34ead58568b4b3d0d62ddd2cd34da40b6d1056cc6a6e1b096030276d6313745844a25998
7
+ data.tar.gz: 51cb295e2097b87d2ba21faf62495087a5cae5a7db695e7703e219cbde57f883b4f776d39ca68258299ba6f33347c1bca3ce1608580b27f5a2e251f978c3c86f
@@ -6,8 +6,7 @@ executors:
6
6
  docker:
7
7
  - image: circleci/ruby:2.6
8
8
  environment:
9
- DB_USER: 'root'
10
- DB_PASS: 'root'
9
+ DB_USER: root
11
10
  DB_HOST: '127.0.0.1'
12
11
  - image: circleci/mysql:8-ram
13
12
  environment:
@@ -15,6 +14,10 @@ executors:
15
14
  MYSQL_ROOT_PASSWORD: root
16
15
  MYSQL_DATABASE: breathing_test
17
16
  command: [--default-authentication-plugin=mysql_native_password]
17
+ - image: circleci/postgres:10.6-alpine-ram
18
+ environment:
19
+ POSTGRES_USER: root
20
+ POSTGRES_DB: breathing_test
18
21
 
19
22
  commands:
20
23
  setup_bundle:
@@ -35,6 +38,9 @@ commands:
35
38
  - run:
36
39
  name: Wait for DB
37
40
  command: dockerize -wait tcp://127.0.0.1:3306 -timeout 1m
41
+ - run:
42
+ name: Wait for DB
43
+ command: dockerize -wait tcp://127.0.0.1:5432 -timeout 1m
38
44
 
39
45
  jobs:
40
46
  test:
@@ -43,7 +49,8 @@ jobs:
43
49
  - checkout
44
50
  - setup_bundle
45
51
  - wait_for_db
46
- - run: bundle exec rspec ./spec
52
+ - run: DB=mysql DB_PASS=root bundle exec rspec ./spec
53
+ - run: DB=pg bundle exec rspec ./spec
47
54
 
48
55
  workflows:
49
56
  version: 2
data/README.md CHANGED
@@ -5,15 +5,8 @@ Logging mechanism using database triggers to store the old and new row states in
5
5
 
6
6
  ## Install
7
7
 
8
- Put this line in your Gemfile:
9
-
10
- ```
11
- gem 'breathing'
12
- ```
13
-
14
- Then bundle:
15
8
  ```
16
- % bundle
9
+ gem install 'breathing'
17
10
  ```
18
11
 
19
12
  ## Usage
@@ -24,6 +17,8 @@ Just run the following command.
24
17
 
25
18
  ```
26
19
  % DATABASE_URL="mysql2://user:pass@host:port/database" breathing install
20
+ or
21
+ % DATABASE_URL="postgres://user:pass@host:port/database" breathing install
27
22
  ```
28
23
 
29
24
  - Create table `change_logs`
@@ -46,12 +41,20 @@ Cleanup command.
46
41
  - change_logs_update_{table_name}
47
42
  - change_logs_delete_{table_name}
48
43
 
49
- ### export
44
+ ### Export
50
45
 
51
46
  ```
52
47
  % DATABASE_URL="mysql2://user:pass@host:port/database" breathing export
53
48
  ```
54
49
 
50
+ - Output file `breathing.xlsx`
51
+
52
+ ## Compatibility
53
+
54
+ - Ruby 2.3.0+
55
+ - MySQL 5.7.0+
56
+ - PostgreSQL 8.0+
57
+
55
58
  ## Copyright
56
59
 
57
60
  Copyright (c) 2020 Akira Kusumoto. See MIT-LICENSE file for further details.
@@ -2,7 +2,7 @@ $:.push File.expand_path('lib', __dir__)
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'breathing'
5
- s.version = '0.0.3'
5
+ s.version = '0.0.4'
6
6
  s.platform = Gem::Platform::RUBY
7
7
  s.authors = ['Akira Kusumoto']
8
8
  s.email = ['akirakusumo10@gmail.com']
@@ -21,8 +21,10 @@ Gem::Specification.new do |s|
21
21
  s.add_runtime_dependency 'thor'
22
22
 
23
23
  s.add_dependency 'activerecord', ['>= 5.0.0']
24
- s.add_dependency 'rubyXL', ['>= 3.4.0']
24
+ s.add_dependency 'hairtrigger'
25
25
  s.add_dependency 'mysql2'
26
+ s.add_dependency 'pg'
27
+ s.add_dependency 'rubyXL', ['>= 3.4.0']
26
28
  s.add_development_dependency 'pry-byebug'
27
29
  s.add_development_dependency 'rspec', '~> 3.9'
28
30
  end
@@ -1,4 +1,5 @@
1
1
  require 'active_record'
2
+ require 'hairtrigger'
2
3
  require 'breathing/installer'
3
4
  require 'breathing/trigger'
4
5
  require 'breathing/change_log'
@@ -3,7 +3,7 @@ require 'breathing'
3
3
 
4
4
  module Breathing
5
5
  class Cli < Thor
6
- default_command :install
6
+ default_command :export
7
7
 
8
8
  desc 'install', 'Create table change_logs and create triggers'
9
9
  def install
@@ -4,8 +4,12 @@ require 'breathing/trigger'
4
4
  require 'breathing/change_log'
5
5
 
6
6
  module Breathing
7
+ class UnsupportedError < StandardError; end
8
+
7
9
  class Installer
8
10
  def install
11
+ raise Breathing::UnsupportedError, "Version MySQL 5.6 is not supported." unless database_version_valid?
12
+
9
13
  create_log_table unless log_table_exists?
10
14
 
11
15
  models.each do |model|
@@ -24,6 +28,11 @@ module Breathing
24
28
 
25
29
  private
26
30
 
31
+ def database_version_valid?
32
+ connection = ActiveRecord::Base.connection
33
+ connection.adapter_name == "PostgreSQL" || (connection.adapter_name == 'Mysql2' && connection.raw_connection.info[:version].to_f >= 5.7)
34
+ end
35
+
27
36
  def log_table_name
28
37
  Breathing::ChangeLog.table_name
29
38
  end
@@ -12,69 +12,61 @@ module Breathing
12
12
 
13
13
  def create
14
14
  trigger_name = "#{log_table_name}_insert_#{model.table_name}"
15
+
15
16
  unless exists?(trigger_name)
16
17
  puts "CREATE TRIGGER #{trigger_name}"
17
- ActiveRecord::Base.connection.execute <<-SQL
18
- CREATE TRIGGER #{trigger_name}
19
- AFTER INSERT
20
- ON #{model.table_name}
21
- FOR EACH ROW
22
- BEGIN
23
- INSERT INTO #{log_table_name} (`action`, `table_name`, `transaction_id`, `before_data`, `after_data`, `created_at`)
18
+
19
+ ActiveRecord::Base.connection.create_trigger(trigger_name).on(model.table_name).after(:insert) do
20
+ <<-SQL
21
+ INSERT INTO #{log_table_name} (action, table_name, transaction_id, before_data, after_data, created_at)
24
22
  VALUES ('INSERT', '#{model.table_name}', NEW.id,
25
- JSON_OBJECT(),
26
- JSON_OBJECT(#{json_object_values(model.columns, 'NEW')}),
23
+ '{}',
24
+ #{row_to_json(model.columns, 'NEW')},
27
25
  CURRENT_TIMESTAMP);
28
- END;
29
- SQL
26
+ SQL
27
+ end
30
28
  end
31
29
 
32
30
  trigger_name = "#{log_table_name}_update_#{model.table_name}"
33
31
  unless exists?(trigger_name)
34
32
  puts "CREATE TRIGGER #{trigger_name}"
35
- ActiveRecord::Base.connection.execute <<-SQL
36
- CREATE TRIGGER #{trigger_name}
37
- BEFORE UPDATE
38
- ON #{model.table_name}
39
- FOR EACH ROW
40
- BEGIN
41
- IF (OLD.updated_at != NEW.updated_at) THEN
42
- INSERT INTO #{log_table_name} (`action`, `table_name`, `transaction_id`, `before_data`, `after_data`, `created_at`)
33
+
34
+ ActiveRecord::Base.connection.create_trigger(trigger_name).on(model.table_name).before(:update).of(:updated_at) do
35
+ <<-SQL
36
+ INSERT INTO #{log_table_name} (action, table_name, transaction_id, before_data, after_data, created_at)
43
37
  VALUES ('UPDATE', '#{model.table_name}', NEW.id,
44
- JSON_OBJECT(#{json_object_values(model.columns, 'OLD')}),
45
- JSON_OBJECT(#{json_object_values(model.columns, 'NEW')}),
38
+ #{row_to_json(model.columns, 'OLD')},
39
+ #{row_to_json(model.columns, 'NEW')},
46
40
  CURRENT_TIMESTAMP);
47
- end if;
48
- END;
49
- SQL
41
+ SQL
42
+ end
50
43
  end
51
44
 
52
45
  trigger_name = "#{log_table_name}_delete_#{model.table_name}"
53
46
  unless exists?(trigger_name)
54
47
  puts "CREATE TRIGGER #{trigger_name}"
55
- ActiveRecord::Base.connection.execute <<-SQL
56
- CREATE TRIGGER #{trigger_name}
57
- AFTER DELETE
58
- ON #{model.table_name}
59
- FOR EACH ROW
60
- BEGIN
61
- INSERT INTO #{log_table_name} (`action`, `table_name`, `transaction_id`, `before_data`, `after_data`, `created_at`)
62
- VALUES ('DELETE', '#{model.table_name}', OLD.id,
63
- JSON_OBJECT(#{json_object_values(model.columns, 'OLD')}),
64
- JSON_OBJECT(),
48
+ ActiveRecord::Base.connection.create_trigger(trigger_name).on(model.table_name).after(:delete) do
49
+ <<-SQL
50
+ INSERT INTO #{log_table_name} (action, table_name, transaction_id, before_data, after_data, created_at)
51
+ VALUES ('DELETE', '#{model.table_name}', OLD.id,
52
+ #{row_to_json(model.columns, 'OLD')},
53
+ '{}',
65
54
  CURRENT_TIMESTAMP);
66
- END;
67
- SQL
55
+ SQL
56
+ end
68
57
  end
69
58
  end
70
59
 
71
60
  def drop
72
61
  %w[insert update delete].each do |action|
73
62
  trigger_name = "#{log_table_name}_#{action}_#{model.table_name}"
74
- next unless exists?(trigger_name)
75
63
 
76
64
  begin
77
- sql = "DROP TRIGGER #{trigger_name}"
65
+ sql = "DROP TRIGGER IF EXISTS #{trigger_name}"
66
+ if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
67
+ sql << " ON #{model.table_name} CASCADE;"
68
+ sql << " DROP FUNCTION IF EXISTS #{trigger_name} CASCADE;"
69
+ end
78
70
  puts sql
79
71
  ActiveRecord::Base.connection.execute(sql)
80
72
  rescue StandardError => e
@@ -86,15 +78,19 @@ module Breathing
86
78
  private
87
79
 
88
80
  def exists?(trigger_name)
89
- @trigger_names ||= ActiveRecord::Base.connection.select_rows('SHOW TRIGGERS').map { |row| row.first }
90
- @trigger_names.include?(trigger_name)
81
+ ActiveRecord::Base.connection.triggers.keys.include?(trigger_name)
91
82
  end
92
83
 
93
- def json_object_values(columns, state)
94
- columns.each.with_object([]) do |column, array|
95
- array << "'#{column.name}'"
96
- array << "#{state}.#{column.name}"
97
- end.join(',')
84
+ def row_to_json(columns, state)
85
+ if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
86
+ "row_to_json(#{state}.*)"
87
+ else
88
+ json_object_values = columns.each.with_object([]) do |column, array|
89
+ array << "'#{column.name}'"
90
+ array << "#{state}.#{column.name}"
91
+ end
92
+ "JSON_OBJECT(#{json_object_values.join(',')})"
93
+ end
98
94
  end
99
95
  end
100
96
  end
@@ -2,7 +2,7 @@ require 'active_record'
2
2
  require 'breathing'
3
3
 
4
4
  ActiveRecord::Base.establish_connection(
5
- YAML.load(ERB.new(File.read('spec/database.yml')).result)['test']
5
+ YAML.load(ERB.new(File.read('spec/database.yml')).result)["test_#{ENV['DB'] || 'mysql'}"]
6
6
  )
7
7
 
8
8
  ActiveRecord::Schema.define version: 0 do
@@ -3,7 +3,9 @@ require 'spec_helper'
3
3
  describe Breathing::Excel do
4
4
  describe '#create' do
5
5
  before { Breathing::Installer.new.install }
6
- after { Breathing::Installer.new.uninstall }
6
+ after do
7
+ Breathing::Installer.new.uninstall if ActiveRecord::Base.connection.adapter_name == "Mysql2"
8
+ end
7
9
 
8
10
  it do
9
11
  user = User.create!(name: 'a', age: 20)
@@ -5,36 +5,41 @@ describe Breathing do
5
5
  expect(Breathing::VERSION).not_to be nil
6
6
  end
7
7
 
8
- before { Breathing::Installer.new.install }
9
- after { Breathing::Installer.new.uninstall }
10
-
11
- it do
12
- expect(Breathing::ChangeLog.count).to eq(0)
13
-
14
- # INSERT
15
- user = User.create!(name: 'a', age: 20)
16
- expect(Breathing::ChangeLog.count).to eq(1)
17
-
18
- log = Breathing::ChangeLog.where(table_name: user.class.table_name, transaction_id: user.id).last
19
- expect(log.before_data).to eq({})
20
- expect(log.after_data['name']).to eq('a')
21
- expect(log.after_data['age']).to eq(20)
22
-
23
- # UPDATE
24
- user.update!(age: 21)
25
- expect(Breathing::ChangeLog.count).to eq(2)
26
-
27
- log = Breathing::ChangeLog.where(table_name: user.class.table_name, transaction_id: user.id).last
28
- expect(log.before_data['age']).to eq(20)
29
- expect(log.after_data['age']).to eq(21)
30
- expect(log.before_data['name']).to eq(log.after_data['name'])
31
- expect(log.changed_attribute_columns).to eq(%w[age updated_at])
32
-
33
- # DELETE
34
- user.destroy!
35
- expect(Breathing::ChangeLog.count).to eq(3)
36
- log = Breathing::ChangeLog.where(table_name: user.class.table_name, transaction_id: user.id).last
37
- expect(log.before_data['name']).to eq('a')
38
- expect(log.after_data).to eq({})
8
+ describe 'change_logs' do
9
+ before { Breathing::Installer.new.install }
10
+
11
+ after do
12
+ Breathing::Installer.new.uninstall if ActiveRecord::Base.connection.adapter_name == "Mysql2"
13
+ end
14
+
15
+ it do
16
+ expect(Breathing::ChangeLog.count).to eq(0)
17
+
18
+ # INSERT
19
+ user = User.create!(name: 'a', age: 20)
20
+ expect(Breathing::ChangeLog.count).to eq(1)
21
+
22
+ log = Breathing::ChangeLog.where(table_name: user.class.table_name, transaction_id: user.id).last
23
+ expect(log.before_data).to eq({})
24
+ expect(log.after_data['name']).to eq('a')
25
+ expect(log.after_data['age']).to eq(20)
26
+
27
+ # UPDATE
28
+ user.update!(age: 21)
29
+ expect(Breathing::ChangeLog.count).to eq(2)
30
+
31
+ log = Breathing::ChangeLog.where(table_name: user.class.table_name, transaction_id: user.id).last
32
+ expect(log.before_data['age']).to eq(20)
33
+ expect(log.after_data['age']).to eq(21)
34
+ expect(log.before_data['name']).to eq(log.after_data['name'])
35
+ expect(log.changed_attribute_columns).to eq(%w[age updated_at])
36
+
37
+ # DELETE
38
+ user.destroy!
39
+ expect(Breathing::ChangeLog.count).to eq(3)
40
+ log = Breathing::ChangeLog.where(table_name: user.class.table_name, transaction_id: user.id).last
41
+ expect(log.before_data['name']).to eq('a')
42
+ expect(log.after_data).to eq({})
43
+ end
39
44
  end
40
45
  end
@@ -1,11 +1,18 @@
1
- test:
1
+ test_mysql:
2
2
  adapter: mysql2
3
3
  encoding: utf8mb4
4
4
  charset: utf8mb4
5
5
  collation: utf8mb4_general_ci
6
- pool: 5
7
6
  username: <%= ENV.fetch("DB_USER") { 'root' } %>
8
7
  password: <%= ENV.fetch("DB_PASS") { '' } %>
9
8
  host: <%= ENV.fetch("DB_HOST") { '127.0.0.1' } %>
10
9
  socket: /tmp/mysql.sock
11
- database: breathing_test
10
+ database: breathing_test
11
+
12
+ test_pg:
13
+ adapter: postgresql
14
+ encoding: unicode
15
+ username: <%= ENV.fetch("DB_USER") { 'root' } %>
16
+ password: <%= ENV.fetch("DB_PASS") { '' } %>
17
+ host: <%= ENV.fetch("DB_HOST") { '127.0.0.1' } %>
18
+ database: breathing_test
@@ -4,6 +4,8 @@ require 'active_record'
4
4
  require 'breathing'
5
5
  require_relative 'app'
6
6
 
7
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
8
+
7
9
  RSpec.configure do |config|
8
10
  config.around do |example|
9
11
  ActiveRecord::Base.transaction do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: breathing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Akira Kusumoto
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-11 00:00:00.000000000 Z
11
+ date: 2020-12-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -39,19 +39,19 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: 5.0.0
41
41
  - !ruby/object:Gem::Dependency
42
- name: rubyXL
42
+ name: hairtrigger
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 3.4.0
47
+ version: '0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 3.4.0
54
+ version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: mysql2
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +66,34 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pg
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubyXL
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 3.4.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 3.4.0
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: pry-byebug
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -125,7 +153,7 @@ homepage: https://github.com/bluerabbit/breathing
125
153
  licenses:
126
154
  - MIT
127
155
  metadata: {}
128
- post_install_message:
156
+ post_install_message:
129
157
  rdoc_options: []
130
158
  require_paths:
131
159
  - lib
@@ -141,7 +169,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
169
  version: '0'
142
170
  requirements: []
143
171
  rubygems_version: 3.0.3
144
- signing_key:
172
+ signing_key:
145
173
  specification_version: 4
146
174
  summary: Audit logging for database
147
175
  test_files: