fx 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +27 -0
- data/.yardopts +4 -0
- data/Appraisals +21 -0
- data/CONTRIBUTING.md +15 -0
- data/Gemfile +4 -0
- data/README.md +81 -0
- data/Rakefile +23 -0
- data/bin/appraisal +17 -0
- data/bin/console +14 -0
- data/bin/rake +17 -0
- data/bin/rspec +17 -0
- data/bin/setup +12 -0
- data/bin/yard +17 -0
- data/fx.gemspec +38 -0
- data/gemfiles/rails40.gemfile +8 -0
- data/gemfiles/rails40.gemfile.lock +111 -0
- data/gemfiles/rails41.gemfile +8 -0
- data/gemfiles/rails41.gemfile.lock +113 -0
- data/gemfiles/rails42.gemfile +8 -0
- data/gemfiles/rails42.gemfile.lock +130 -0
- data/gemfiles/rails50.gemfile +8 -0
- data/gemfiles/rails50.gemfile.lock +126 -0
- data/lib/fx.rb +21 -0
- data/lib/fx/adapters/postgres.rb +142 -0
- data/lib/fx/adapters/postgres/connection.rb +16 -0
- data/lib/fx/adapters/postgres/functions.rb +55 -0
- data/lib/fx/adapters/postgres/triggers.rb +56 -0
- data/lib/fx/command_recorder.rb +29 -0
- data/lib/fx/command_recorder/arguments.rb +43 -0
- data/lib/fx/command_recorder/function.rb +30 -0
- data/lib/fx/command_recorder/trigger.rb +30 -0
- data/lib/fx/configuration.rb +38 -0
- data/lib/fx/definition.rb +36 -0
- data/lib/fx/function.rb +24 -0
- data/lib/fx/schema_dumper.rb +15 -0
- data/lib/fx/schema_dumper/function.rb +29 -0
- data/lib/fx/schema_dumper/trigger.rb +29 -0
- data/lib/fx/statements.rb +16 -0
- data/lib/fx/statements/function.rb +105 -0
- data/lib/fx/statements/trigger.rb +133 -0
- data/lib/fx/trigger.rb +24 -0
- data/lib/fx/version.rb +4 -0
- data/lib/generators.rb +11 -0
- data/lib/generators/fx/function/USAGE +9 -0
- data/lib/generators/fx/function/function_generator.rb +98 -0
- data/lib/generators/fx/function/templates/db/migrate/create_function.erb +5 -0
- data/lib/generators/fx/function/templates/db/migrate/update_function.erb +5 -0
- data/lib/generators/fx/trigger/USAGE +18 -0
- data/lib/generators/fx/trigger/templates/db/migrate/create_trigger.erb +5 -0
- data/lib/generators/fx/trigger/templates/db/migrate/update_trigger.erb +5 -0
- data/lib/generators/fx/trigger/trigger_generator.rb +108 -0
- data/spec/acceptance/user_manages_functions_spec.rb +37 -0
- data/spec/acceptance/user_manages_triggers_spec.rb +51 -0
- data/spec/acceptance_helper.rb +61 -0
- data/spec/dummy/.gitignore +16 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +15 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +9 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/db/migrate/.keep +0 -0
- data/spec/features/functions/migrations_spec.rb +65 -0
- data/spec/features/functions/revert_spec.rb +75 -0
- data/spec/features/triggers/migrations_spec.rb +56 -0
- data/spec/features/triggers/revert_spec.rb +95 -0
- data/spec/fx/adapters/postgres_spec.rb +149 -0
- data/spec/fx/command_recorder/arguments_spec.rb +41 -0
- data/spec/fx/command_recorder_spec.rb +171 -0
- data/spec/fx/configuration_spec.rb +21 -0
- data/spec/fx/definition_spec.rb +111 -0
- data/spec/fx/schema_dumper/function_spec.rb +22 -0
- data/spec/fx/schema_dumper/trigger_spec.rb +40 -0
- data/spec/fx/statements/function_spec.rb +103 -0
- data/spec/fx/statements/trigger_spec.rb +132 -0
- data/spec/generators/fx/function/function_generator_spec.rb +34 -0
- data/spec/generators/fx/trigger/trigger_generator_spec.rb +47 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/definition_helpers.rb +37 -0
- data/spec/support/generator_setup.rb +11 -0
- data/spec/support/migration_helpers.rb +17 -0
- metadata +334 -0
@@ -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
|
data/spec/dummy/Rakefile
ADDED
data/spec/dummy/bin/rake
ADDED
@@ -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
|
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
|