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