fx 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +27 -0
  6. data/.yardopts +4 -0
  7. data/Appraisals +21 -0
  8. data/CONTRIBUTING.md +15 -0
  9. data/Gemfile +4 -0
  10. data/README.md +81 -0
  11. data/Rakefile +23 -0
  12. data/bin/appraisal +17 -0
  13. data/bin/console +14 -0
  14. data/bin/rake +17 -0
  15. data/bin/rspec +17 -0
  16. data/bin/setup +12 -0
  17. data/bin/yard +17 -0
  18. data/fx.gemspec +38 -0
  19. data/gemfiles/rails40.gemfile +8 -0
  20. data/gemfiles/rails40.gemfile.lock +111 -0
  21. data/gemfiles/rails41.gemfile +8 -0
  22. data/gemfiles/rails41.gemfile.lock +113 -0
  23. data/gemfiles/rails42.gemfile +8 -0
  24. data/gemfiles/rails42.gemfile.lock +130 -0
  25. data/gemfiles/rails50.gemfile +8 -0
  26. data/gemfiles/rails50.gemfile.lock +126 -0
  27. data/lib/fx.rb +21 -0
  28. data/lib/fx/adapters/postgres.rb +142 -0
  29. data/lib/fx/adapters/postgres/connection.rb +16 -0
  30. data/lib/fx/adapters/postgres/functions.rb +55 -0
  31. data/lib/fx/adapters/postgres/triggers.rb +56 -0
  32. data/lib/fx/command_recorder.rb +29 -0
  33. data/lib/fx/command_recorder/arguments.rb +43 -0
  34. data/lib/fx/command_recorder/function.rb +30 -0
  35. data/lib/fx/command_recorder/trigger.rb +30 -0
  36. data/lib/fx/configuration.rb +38 -0
  37. data/lib/fx/definition.rb +36 -0
  38. data/lib/fx/function.rb +24 -0
  39. data/lib/fx/schema_dumper.rb +15 -0
  40. data/lib/fx/schema_dumper/function.rb +29 -0
  41. data/lib/fx/schema_dumper/trigger.rb +29 -0
  42. data/lib/fx/statements.rb +16 -0
  43. data/lib/fx/statements/function.rb +105 -0
  44. data/lib/fx/statements/trigger.rb +133 -0
  45. data/lib/fx/trigger.rb +24 -0
  46. data/lib/fx/version.rb +4 -0
  47. data/lib/generators.rb +11 -0
  48. data/lib/generators/fx/function/USAGE +9 -0
  49. data/lib/generators/fx/function/function_generator.rb +98 -0
  50. data/lib/generators/fx/function/templates/db/migrate/create_function.erb +5 -0
  51. data/lib/generators/fx/function/templates/db/migrate/update_function.erb +5 -0
  52. data/lib/generators/fx/trigger/USAGE +18 -0
  53. data/lib/generators/fx/trigger/templates/db/migrate/create_trigger.erb +5 -0
  54. data/lib/generators/fx/trigger/templates/db/migrate/update_trigger.erb +5 -0
  55. data/lib/generators/fx/trigger/trigger_generator.rb +108 -0
  56. data/spec/acceptance/user_manages_functions_spec.rb +37 -0
  57. data/spec/acceptance/user_manages_triggers_spec.rb +51 -0
  58. data/spec/acceptance_helper.rb +61 -0
  59. data/spec/dummy/.gitignore +16 -0
  60. data/spec/dummy/Rakefile +6 -0
  61. data/spec/dummy/bin/bundle +3 -0
  62. data/spec/dummy/bin/rails +4 -0
  63. data/spec/dummy/bin/rake +4 -0
  64. data/spec/dummy/config.ru +4 -0
  65. data/spec/dummy/config/application.rb +15 -0
  66. data/spec/dummy/config/boot.rb +5 -0
  67. data/spec/dummy/config/database.yml +9 -0
  68. data/spec/dummy/config/environment.rb +5 -0
  69. data/spec/dummy/db/migrate/.keep +0 -0
  70. data/spec/features/functions/migrations_spec.rb +65 -0
  71. data/spec/features/functions/revert_spec.rb +75 -0
  72. data/spec/features/triggers/migrations_spec.rb +56 -0
  73. data/spec/features/triggers/revert_spec.rb +95 -0
  74. data/spec/fx/adapters/postgres_spec.rb +149 -0
  75. data/spec/fx/command_recorder/arguments_spec.rb +41 -0
  76. data/spec/fx/command_recorder_spec.rb +171 -0
  77. data/spec/fx/configuration_spec.rb +21 -0
  78. data/spec/fx/definition_spec.rb +111 -0
  79. data/spec/fx/schema_dumper/function_spec.rb +22 -0
  80. data/spec/fx/schema_dumper/trigger_spec.rb +40 -0
  81. data/spec/fx/statements/function_spec.rb +103 -0
  82. data/spec/fx/statements/trigger_spec.rb +132 -0
  83. data/spec/generators/fx/function/function_generator_spec.rb +34 -0
  84. data/spec/generators/fx/trigger/trigger_generator_spec.rb +47 -0
  85. data/spec/spec_helper.rb +21 -0
  86. data/spec/support/definition_helpers.rb +37 -0
  87. data/spec/support/generator_setup.rb +11 -0
  88. data/spec/support/migration_helpers.rb +17 -0
  89. metadata +334 -0
@@ -0,0 +1,5 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ update_trigger <%= formatted_name %>, on: <%= formatted_table_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
4
+ end
5
+ end
@@ -0,0 +1,108 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module Fx
5
+ module Generators
6
+ # @api private
7
+ class TriggerGenerator < Rails::Generators::NamedBase
8
+ include Rails::Generators::Migration
9
+ source_root File.expand_path("../templates", __FILE__)
10
+ argument :table_name, type: :hash, required: true
11
+
12
+ def create_triggers_directory
13
+ unless trigger_definition_path.exist?
14
+ empty_directory(trigger_definition_path)
15
+ end
16
+ end
17
+
18
+ def create_trigger_definition
19
+ create_file definition.path
20
+ end
21
+
22
+ def create_migration_file
23
+ if updating_existing_trigger?
24
+ migration_template(
25
+ "db/migrate/update_trigger.erb",
26
+ "db/migrate/update_trigger_#{file_name}_to_version_#{version}.rb"
27
+ )
28
+ else
29
+ migration_template(
30
+ "db/migrate/create_trigger.erb",
31
+ "db/migrate/create_trigger_#{file_name}.rb"
32
+ )
33
+ end
34
+ end
35
+
36
+ def self.next_migration_number(dir)
37
+ ::ActiveRecord::Generators::Base.next_migration_number(dir)
38
+ end
39
+
40
+ no_tasks do
41
+ def previous_version
42
+ @_previous_version ||= Dir.entries(trigger_definition_path).
43
+ map { |name| version_regex.match(name).try(:[], "version").to_i }.
44
+ max
45
+ end
46
+
47
+ def version
48
+ @_version ||= previous_version.next
49
+ end
50
+
51
+ def migration_class_name
52
+ if updating_existing_trigger?
53
+ "UpdateTrigger#{class_name}ToVersion#{version}"
54
+ else
55
+ super
56
+ end
57
+ end
58
+
59
+ def formatted_name
60
+ if singular_name.include?(".")
61
+ "\"#{singular_name}\""
62
+ else
63
+ ":#{singular_name}"
64
+ end
65
+ end
66
+
67
+ def formatted_table_name
68
+ name = table_name["table_name"] || table_name["on"]
69
+
70
+ if name.nil?
71
+ raise(
72
+ ArgumentError,
73
+ "Either `table_name:NAME` or `on:NAME` must be specified",
74
+ )
75
+ end
76
+
77
+ if name.include?(".")
78
+ "\"#{name}\""
79
+ else
80
+ ":#{name}"
81
+ end
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def version_regex
88
+ /\A#{file_name}_v(?<version>\d+)\.sql\z/
89
+ end
90
+
91
+ def updating_existing_trigger?
92
+ previous_version > 0
93
+ end
94
+
95
+ def definition
96
+ Fx::Definition.new(
97
+ name: file_name,
98
+ version: version,
99
+ type: "trigger",
100
+ )
101
+ end
102
+
103
+ def trigger_definition_path
104
+ @_trigger_definition_path ||= Rails.root.join(*["db", "triggers"])
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,37 @@
1
+ require "acceptance_helper"
2
+
3
+ describe "User manages functions" do
4
+ it "handles simple functions" do
5
+ successfully "rails generate fx:function test"
6
+ write_function_definition "test_v01", <<~EOS
7
+ CREATE OR REPLACE FUNCTION test()
8
+ RETURNS text AS $$
9
+ BEGIN
10
+ RETURN 'test';
11
+ END;
12
+ $$ LANGUAGE plpgsql;
13
+ EOS
14
+ successfully "rake db:migrate"
15
+
16
+ result = execute("SELECT * FROM test() AS result")
17
+ expect(result).to eq("result" => "test")
18
+
19
+ successfully "rails generate fx:function test"
20
+ verify_identical_definitions(
21
+ "db/functions/test_v01.sql",
22
+ "db/functions/test_v02.sql",
23
+ )
24
+ write_function_definition "test_v02", <<~EOS
25
+ CREATE OR REPLACE FUNCTION test()
26
+ RETURNS text AS $$
27
+ BEGIN
28
+ RETURN 'testest';
29
+ END;
30
+ $$ LANGUAGE plpgsql;
31
+ EOS
32
+ successfully "rake db:migrate"
33
+
34
+ result = execute("SELECT * FROM test() AS result")
35
+ expect(result).to eq("result" => "testest")
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ require "acceptance_helper"
2
+
3
+ describe "User manages triggers" do
4
+ it "handles simple triggers" do
5
+ successfully "rails generate model user name:string upper_name:string"
6
+ successfully "rails generate fx:function uppercase_users_name"
7
+ write_function_definition "uppercase_users_name_v01", <<~EOS
8
+ CREATE OR REPLACE FUNCTION uppercase_users_name()
9
+ RETURNS trigger AS $$
10
+ BEGIN
11
+ NEW.upper_name = UPPER(NEW.name);
12
+ RETURN NEW;
13
+ END;
14
+ $$ LANGUAGE plpgsql;
15
+ EOS
16
+ successfully "rails generate fx:trigger uppercase_users_name table_name:users"
17
+ write_trigger_definition "uppercase_users_name_v01", <<~EOS
18
+ CREATE TRIGGER uppercase_users_name
19
+ BEFORE INSERT ON users
20
+ FOR EACH ROW
21
+ EXECUTE PROCEDURE uppercase_users_name();
22
+ EOS
23
+ successfully "rake db:migrate"
24
+
25
+ execute <<~SQL
26
+ INSERT INTO users
27
+ (name, created_at, updated_at)
28
+ VALUES
29
+ ('Bob', NOW(), NOW());
30
+ SQL
31
+ result = execute("SELECT upper_name FROM users WHERE name = 'Bob';")
32
+ expect(result).to eq("upper_name" => "BOB")
33
+
34
+ successfully "rails generate fx:trigger uppercase_users_name table_name:users"
35
+ write_trigger_definition "uppercase_users_name_v02", <<~EOS
36
+ CREATE TRIGGER uppercase_users_name
37
+ BEFORE UPDATE ON users
38
+ FOR EACH ROW
39
+ EXECUTE PROCEDURE uppercase_users_name();
40
+ EOS
41
+ successfully "rake db:migrate"
42
+ execute <<~EOS
43
+ UPDATE users
44
+ SET name = 'Alice'
45
+ WHERE id = 1;
46
+ EOS
47
+
48
+ result = execute("SELECT upper_name FROM users WHERE name = 'Alice';")
49
+ expect(result).to eq("upper_name" => "ALICE")
50
+ end
51
+ end
@@ -0,0 +1,61 @@
1
+ require "bundler"
2
+
3
+ ENV["RAILS_ENV"] = "test"
4
+
5
+ RSpec.configure do |config|
6
+ config.around(:each) do |example|
7
+ Dir.chdir("spec/dummy") do
8
+ example.run
9
+ end
10
+ end
11
+
12
+ config.before(:suite) do
13
+ Dir.chdir("spec/dummy") do
14
+ system <<-CMD
15
+ git init 1>/dev/null &&
16
+ git add -A &&
17
+ git commit --no-gpg-sign --message 'initial' 1>/dev/null
18
+ CMD
19
+ end
20
+ end
21
+
22
+ config.after(:suite) do
23
+ Dir.chdir("spec/dummy") do
24
+ ActiveRecord::Base.connection.disconnect!
25
+ system <<-CMD
26
+ rake db:drop db:create &&
27
+ git add -A &&
28
+ git reset --hard HEAD 1>/dev/null &&
29
+ rm -rf .git/ 1>/dev/null
30
+ CMD
31
+ end
32
+ end
33
+
34
+ def successfully(command)
35
+ `RAILS_ENV=test #{command}`
36
+ expect($?.exitstatus).to eq(0), "'#{command}' was unsuccessful"
37
+ end
38
+
39
+ def write_function_definition(file, contents)
40
+ write_definition(file, contents, "functions")
41
+ end
42
+
43
+ def write_trigger_definition(file, contents)
44
+ write_definition(file, contents, "triggers")
45
+ end
46
+
47
+ def write_definition(file, contents, directory)
48
+ File.open("db/#{directory}/#{file}.sql", File::WRONLY) do |definition|
49
+ definition.truncate(0)
50
+ definition.write(contents)
51
+ end
52
+ end
53
+
54
+ def verify_identical_definitions(def_a, def_b)
55
+ successfully "cmp #{def_a} #{def_b}"
56
+ end
57
+
58
+ def execute(command)
59
+ ActiveRecord::Base.connection.execute(command).first
60
+ end
61
+ end
@@ -0,0 +1,16 @@
1
+ # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2
+ #
3
+ # If you find yourself ignoring temporary files generated by your text editor
4
+ # or operating system, you probably want to add a global ignore instead:
5
+ # git config --global core.excludesfile '~/.gitignore_global'
6
+
7
+ # Ignore bundler config.
8
+ /.bundle
9
+
10
+ # Ignore the default SQLite database.
11
+ /db/*.sqlite3
12
+ /db/*.sqlite3-journal
13
+
14
+ # Ignore all logfiles and tempfiles.
15
+ /log/*.log
16
+ /tmp
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
+ load Gem.bin_path('bundler', 'bundle')
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
3
+ require_relative '../config/boot'
4
+ require 'rails/commands'
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../config/boot'
3
+ require 'rake'
4
+ Rake.application.run
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Rails.application
@@ -0,0 +1,15 @@
1
+ require File.expand_path("../boot", __FILE__)
2
+
3
+ # Pick the frameworks you want:
4
+ require "active_record/railtie"
5
+
6
+ Bundler.require(*Rails.groups)
7
+ require "fx"
8
+
9
+ module Dummy
10
+ class Application < Rails::Application
11
+ config.cache_classes = true
12
+ config.eager_load = false
13
+ config.active_support.deprecation = :stderr
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # Set up gems listed in the Gemfile.
2
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../../Gemfile", __FILE__)
3
+
4
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
5
+ $LOAD_PATH.unshift File.expand_path("../../../../lib", __FILE__)
@@ -0,0 +1,9 @@
1
+ development: &default
2
+ adapter: postgresql
3
+ database: dummy_development
4
+ encoding: unicode
5
+ pool: 5
6
+
7
+ test:
8
+ <<: *default
9
+ database: dummy_test
@@ -0,0 +1,5 @@
1
+ # Load the Rails application.
2
+ require File.expand_path("../application", __FILE__)
3
+
4
+ # Initialize the Rails application.
5
+ Rails.application.initialize!
File without changes
@@ -0,0 +1,65 @@
1
+ require "spec_helper"
2
+
3
+ describe "Function migrations", :db do
4
+ around do |example|
5
+ sql_definition = <<~EOS
6
+ CREATE OR REPLACE FUNCTION test()
7
+ RETURNS text AS $$
8
+ BEGIN
9
+ RETURN 'test';
10
+ END;
11
+ $$ LANGUAGE plpgsql;
12
+ EOS
13
+ with_function_definition(name: :test, sql_definition: sql_definition) do
14
+ example.run
15
+ end
16
+ end
17
+
18
+ it "can run migrations that create functions" do
19
+ migration = Class.new(ActiveRecord::Migration) do
20
+ def up
21
+ create_function :test
22
+ end
23
+ end
24
+
25
+ expect { run_migration(migration, :up) }.not_to raise_error
26
+ end
27
+
28
+ it "can run migrations that drop functions" do
29
+ connection.create_function(:test)
30
+
31
+ migration = Class.new(ActiveRecord::Migration) do
32
+ def up
33
+ drop_function :test
34
+ end
35
+ end
36
+
37
+ expect { run_migration(migration, :up) }.not_to raise_error
38
+ end
39
+
40
+ it "can run migrations that updates functions" do
41
+ connection.create_function(:test)
42
+
43
+ sql_definition = <<~EOS
44
+ CREATE OR REPLACE FUNCTION test()
45
+ RETURNS text AS $$
46
+ BEGIN
47
+ RETURN 'testest';
48
+ END;
49
+ $$ LANGUAGE plpgsql;
50
+ EOS
51
+ with_function_definition(
52
+ name: :test,
53
+ version: 2,
54
+ sql_definition: sql_definition,
55
+ ) do
56
+ migration = Class.new(ActiveRecord::Migration) do
57
+ def change
58
+ update_function :test, version: 2, revert_to_version: 1
59
+ end
60
+ end
61
+
62
+ expect { run_migration(migration, :change) }.not_to raise_error
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,75 @@
1
+ require "spec_helper"
2
+
3
+ describe "Reverting migrations", :db do
4
+ around do |example|
5
+ sql_definition = <<~EOS
6
+ CREATE OR REPLACE FUNCTION test()
7
+ RETURNS text AS $$
8
+ BEGIN
9
+ RETURN 'test';
10
+ END;
11
+ $$ LANGUAGE plpgsql;
12
+ EOS
13
+ with_function_definition(name: :test, sql_definition: sql_definition) do
14
+ example.run
15
+ end
16
+ end
17
+
18
+ it "can run reversible migrations for creating functions" do
19
+ migration = Class.new(ActiveRecord::Migration) do
20
+ def change
21
+ create_function :test
22
+ end
23
+ end
24
+
25
+ expect { run_migration(migration, [:up, :down]) }.not_to raise_error
26
+ end
27
+
28
+ it "can run reversible migrations for dropping functions" do
29
+ connection.create_function(:test)
30
+
31
+ good_migration = Class.new(ActiveRecord::Migration) do
32
+ def change
33
+ drop_function :test, revert_to_version: 1
34
+ end
35
+ end
36
+ bad_migration = Class.new(ActiveRecord::Migration) do
37
+ def change
38
+ drop_function :test
39
+ end
40
+ end
41
+
42
+ expect { run_migration(good_migration, [:up, :down]) }.not_to raise_error
43
+ expect { run_migration(bad_migration, [:up, :down]) }.
44
+ to raise_error(
45
+ ActiveRecord::IrreversibleMigration,
46
+ /`create_function` is reversible only if given a `revert_to_version`/,
47
+ )
48
+ end
49
+
50
+ it "can run reversible migrations for updating functions" do
51
+ connection.create_function(:test)
52
+
53
+ sql_definition = <<~EOS
54
+ CREATE OR REPLACE FUNCTION test()
55
+ RETURNS text AS $$
56
+ BEGIN
57
+ RETURN 'bar';
58
+ END;
59
+ $$ LANGUAGE plpgsql;
60
+ EOS
61
+ with_function_definition(
62
+ name: :test,
63
+ version: 2,
64
+ sql_definition: sql_definition,
65
+ ) do
66
+ migration = Class.new(ActiveRecord::Migration) do
67
+ def change
68
+ update_function :test, version: 2, revert_to_version: 1
69
+ end
70
+ end
71
+
72
+ expect { run_migration(migration, [:up, :down]) }.not_to raise_error
73
+ end
74
+ end
75
+ end