fx 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.
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