breathing 0.0.3 → 0.0.4

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 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: