scenic 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.hound.yml +2 -0
  3. data/.travis.yml +2 -0
  4. data/NEWS.md +8 -0
  5. data/Rakefile +5 -1
  6. data/lib/generators/scenic/view/USAGE +9 -1
  7. data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +5 -0
  8. data/lib/generators/scenic/view/view_generator.rb +55 -5
  9. data/lib/scenic.rb +13 -14
  10. data/lib/scenic/adapters/postgres.rb +27 -0
  11. data/lib/scenic/command_recorder.rb +42 -0
  12. data/lib/scenic/command_recorder/statement_arguments.rb +42 -0
  13. data/lib/scenic/definition.rb +34 -0
  14. data/lib/scenic/schema_dumper.rb +40 -0
  15. data/lib/scenic/statements.rb +84 -0
  16. data/lib/scenic/version.rb +1 -1
  17. data/lib/scenic/view.rb +24 -0
  18. data/spec/dummy/db/migrate/.keep +0 -0
  19. data/spec/generators/scenic/model/model_generator_spec.rb +17 -15
  20. data/spec/generators/scenic/view/view_generator_spec.rb +14 -4
  21. data/spec/scenic/adapters/postgres_spec.rb +38 -0
  22. data/spec/scenic/{active_record/command_recorder → command_recorder}/statement_arguments_spec.rb +1 -1
  23. data/spec/scenic/{active_record/command_recorder_spec.rb → command_recorder_spec.rb} +5 -12
  24. data/spec/scenic/definition_spec.rb +56 -0
  25. data/spec/scenic/{active_record/schema_dumper_spec.rb → schema_dumper_spec.rb} +1 -1
  26. data/spec/scenic/statements_spec.rb +72 -0
  27. data/spec/smoke +86 -0
  28. data/spec/support/view_definition_helpers.rb +3 -3
  29. metadata +27 -14
  30. data/lib/scenic/active_record/command_recorder.rb +0 -44
  31. data/lib/scenic/active_record/command_recorder/statement_arguments.rb +0 -44
  32. data/lib/scenic/active_record/schema_dumper.rb +0 -52
  33. data/lib/scenic/active_record/statements.rb +0 -37
  34. data/spec/scenic/active_record/statements_spec.rb +0 -82
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7f9265748cd49c4ab094b3cb7110b61e0bef6b35
4
- data.tar.gz: c3bd33b0d1ee11d1d807b1a12046839bb8fce53c
3
+ metadata.gz: a764a03ca4024bdd13deea0c9a18251b91e664e1
4
+ data.tar.gz: be3c32867baca5e7c3887372d25f3a40d1f93dd3
5
5
  SHA512:
6
- metadata.gz: 6dd62a29b6991a7c21283764d64c38851ca550a1fa690d8496ee56a6fd0448c81f66c9aaa78e2c9ebabcc430ea1be2b0213f5b7315e05e2c55d4911f5cfb9115
7
- data.tar.gz: 8248642a38d1c583b6883af55a186bea5d2926faa8dcf790996c082ad8d110da74f785db50b4a33bd948fbe6f4b4ea99e7214fdd1afc4641f17d6d2f600b06ec
6
+ metadata.gz: 9afab6aedec2d7167737a4879e8a7f79f3511dc10d0d0b980790cd827035ccec6697c0fb61c401305190f353e72d21543ad2699552ea5eafe7ae0e33877bff68
7
+ data.tar.gz: da72e6957c563eca8c01601ac99a1876d7b48f1767d55d327b5c60ae8acf53853cda13a4b6b1c8d99af72ec9864fa2d90425afc71e41cd5bdb481cebbe251cc9
data/.hound.yml CHANGED
@@ -1,2 +1,4 @@
1
1
  DotPosition:
2
2
  EnforcedStyle: leading
3
+ Style/AlignParameters:
4
+ EnforcedStyle: with_fixed_indentation
data/.travis.yml CHANGED
@@ -1,6 +1,8 @@
1
1
  before_install:
2
2
  - "echo '--colour' > ~/.rspec"
3
3
  - "echo 'gem: --no-document' > ~/.gemrc"
4
+ - git config --global user.name 'Travis CI'
5
+ - git config --global user.email 'travis-ci@example.com'
4
6
  before_script:
5
7
  - pushd spec/dummy && bundle exec rake db:create && popd
6
8
  branches:
data/NEWS.md ADDED
@@ -0,0 +1,8 @@
1
+ * Teach view generator to update existing views [683361d](https://github.com/thoughtbot/scenic/commit/683361d59410f46aba508a3ceb850161dd0be027)
2
+
3
+
4
+ *Caleb Thompson*
5
+
6
+ * Raise an error if view definition is empty. [PR #38](https://github.com/thoughtbot/scenic/issues/38)
7
+
8
+ *Caleb Thompson*
data/Rakefile CHANGED
@@ -3,4 +3,8 @@ require 'rspec/core/rake_task'
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task default: :spec
6
+ task :smoke do
7
+ exec "spec/smoke"
8
+ end
9
+
10
+ task default: [:spec, :smoke]
@@ -2,8 +2,16 @@ Description:
2
2
  Create a new database view for your application. This will create a new
3
3
  view definition file and the accompanying migration.
4
4
 
5
+ If a view of the given name already exists, create a new version of the view
6
+ and a migration to replace the old version with the new.
7
+
5
8
  Examples:
6
9
  rails generate scenic:view searches
7
10
 
8
- create: db/views/searches_v1.sql
11
+ create: db/views/searches_v01.sql
9
12
  create: db/migrate/20140803191158_create_searches.rb
13
+
14
+ rails generate scenic:view searches
15
+
16
+ create: db/views/searches_v02.sql
17
+ create: db/migrate/20140804191158_update_searches_to_version_2.rb
@@ -0,0 +1,5 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ update_view :<%= plural_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
4
+ end
5
+ end
@@ -8,19 +8,69 @@ module Scenic
8
8
  source_root File.expand_path("../templates", __FILE__)
9
9
 
10
10
  def create_view_definition
11
- create_file "db/views/#{plural_name}_v1.sql"
11
+ create_file definition.path
12
12
  end
13
13
 
14
14
  def create_migration_file
15
- migration_template(
16
- "db/migrate/create_view.erb",
17
- "db/migrate/create_#{plural_name}.rb"
18
- )
15
+ if creating_new_view? || destroying_initial_view?
16
+ migration_template(
17
+ "db/migrate/create_view.erb",
18
+ "db/migrate/create_#{plural_file_name}.rb"
19
+ )
20
+ else
21
+ migration_template(
22
+ "db/migrate/update_view.erb",
23
+ "db/migrate/update_#{plural_file_name}_to_version_#{version}.rb"
24
+ )
25
+ end
19
26
  end
20
27
 
21
28
  def self.next_migration_number(dir)
22
29
  ::ActiveRecord::Generators::Base.next_migration_number(dir)
23
30
  end
31
+
32
+ no_tasks do
33
+ def previous_version
34
+ @previous_version ||=
35
+ Dir.entries(Rails.root.join(*%w(db views)))
36
+ .map { |name| version_regex.match(name).try(:[], "version").to_i }
37
+ .max
38
+ end
39
+
40
+ def version
41
+ @version ||= destroying? ? previous_version : previous_version.next
42
+ end
43
+
44
+ def migration_class_name
45
+ if creating_new_view?
46
+ super
47
+ else
48
+ "Update#{class_name.pluralize}ToVersion#{version}"
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def version_regex
56
+ /\A#{plural_file_name}_v(?<version>\d+)\.sql\z/
57
+ end
58
+
59
+ def creating_new_view?
60
+ previous_version == 0
61
+ end
62
+
63
+ def definition
64
+ Scenic::Definition.new(plural_file_name, version)
65
+ end
66
+
67
+ def destroying?
68
+ behavior == :revoke
69
+ end
70
+
71
+ def destroying_initial_view?
72
+ destroying? && version == 1
73
+ end
24
74
  end
25
75
  end
26
76
  end
data/lib/scenic.rb CHANGED
@@ -1,21 +1,20 @@
1
- require "scenic/version"
1
+ require "scenic/adapters/postgres"
2
+ require "scenic/command_recorder"
3
+ require "scenic/definition"
2
4
  require "scenic/railtie"
3
- require "scenic/active_record/command_recorder"
4
- require "scenic/active_record/schema_dumper"
5
- require "scenic/active_record/statements"
5
+ require "scenic/schema_dumper"
6
+ require "scenic/statements"
7
+ require "scenic/version"
8
+ require "scenic/view"
6
9
 
7
10
  module Scenic
8
11
  def self.load
9
- ::ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
10
- include Scenic::ActiveRecord::Statements
11
- end
12
-
13
- ::ActiveRecord::Migration::CommandRecorder.class_eval do
14
- include Scenic::ActiveRecord::CommandRecorder
15
- end
12
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.include Scenic::Statements
13
+ ActiveRecord::Migration::CommandRecorder.include Scenic::CommandRecorder
14
+ ActiveRecord::SchemaDumper.include Scenic::SchemaDumper
15
+ end
16
16
 
17
- ::ActiveRecord::SchemaDumper.class_eval do
18
- include Scenic::ActiveRecord::SchemaDumper
19
- end
17
+ def self.database
18
+ Scenic::Adapters::Postgres
20
19
  end
21
20
  end
@@ -0,0 +1,27 @@
1
+ module Scenic
2
+ module Adapters
3
+ module Postgres
4
+ def self.views
5
+ execute(<<-SQL).map { |result| Scenic::View.new(result) }
6
+ SELECT viewname, definition
7
+ FROM pg_views
8
+ WHERE schemaname = ANY (current_schemas(false))
9
+ SQL
10
+ end
11
+
12
+ def self.create_view(name, sql_definition)
13
+ execute "CREATE VIEW #{name} AS #{sql_definition};"
14
+ end
15
+
16
+ def self.drop_view(name)
17
+ execute "DROP VIEW #{name};"
18
+ end
19
+
20
+ private
21
+
22
+ def self.execute(sql, base = ActiveRecord::Base)
23
+ base.connection.execute sql
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ require "scenic/command_recorder/statement_arguments"
2
+
3
+ module Scenic
4
+ module CommandRecorder
5
+ def create_view(*args)
6
+ record(:create_view, args)
7
+ end
8
+
9
+ def drop_view(*args)
10
+ record(:drop_view, args)
11
+ end
12
+
13
+ def update_view(*args)
14
+ record(:update_view, args)
15
+ end
16
+
17
+ def invert_create_view(args)
18
+ [:drop_view, args]
19
+ end
20
+
21
+ def invert_drop_view(args)
22
+ perform_scenic_inversion(:create_view, args)
23
+ end
24
+
25
+ def invert_update_view(args)
26
+ perform_scenic_inversion(:update_view, args)
27
+ end
28
+
29
+ private
30
+
31
+ def perform_scenic_inversion(method, args)
32
+ scenic_args = StatementArguments.new(args)
33
+
34
+ if scenic_args.revert_to_version.nil?
35
+ message = "#{method} is reversible only if given a revert_to_version"
36
+ raise ActiveRecord::IrreversibleMigration, message
37
+ end
38
+
39
+ [method, scenic_args.invert_version.to_a]
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ module Scenic
2
+ module CommandRecorder
3
+ class StatementArguments
4
+ def initialize(args)
5
+ @args = args.freeze
6
+ end
7
+
8
+ def view
9
+ @args[0]
10
+ end
11
+
12
+ def version
13
+ options[:version]
14
+ end
15
+
16
+ def revert_to_version
17
+ options[:revert_to_version]
18
+ end
19
+
20
+ def invert_version
21
+ StatementArguments.new([view, options_for_revert])
22
+ end
23
+
24
+ def to_a
25
+ @args.to_a
26
+ end
27
+
28
+ private
29
+
30
+ def options
31
+ @options ||= @args[1] || {}
32
+ end
33
+
34
+ def options_for_revert
35
+ options.clone.tap do |revert_options|
36
+ revert_options[:version] = revert_to_version
37
+ revert_options.delete(:revert_to_version)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,34 @@
1
+ module Scenic
2
+ class Definition
3
+ def initialize(name, version)
4
+ @name = name
5
+ @version = version.to_i
6
+ end
7
+
8
+ def to_sql
9
+ File.read(full_path).tap do |content|
10
+ if content.empty?
11
+ raise "Define view query in #{@path} before migrating."
12
+ end
13
+ end
14
+ end
15
+
16
+ def full_path
17
+ Rails.root.join(path)
18
+ end
19
+
20
+ def path
21
+ File.join("db", "views", filename)
22
+ end
23
+
24
+ def version
25
+ @version.to_s.rjust(2, "0")
26
+ end
27
+
28
+ private
29
+
30
+ def filename
31
+ "#{@name}_v#{version}.sql"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ require "rails"
2
+
3
+ module Scenic
4
+ module SchemaDumper
5
+ extend ActiveSupport::Concern
6
+
7
+ included { alias_method_chain :tables, :views }
8
+
9
+ def tables_with_views(stream)
10
+ tables_without_views(stream)
11
+ views(stream)
12
+ end
13
+
14
+ def views(stream)
15
+ views_in_database.select { |view| !ignored?(view.name) }.each do |view|
16
+ stream.puts(view.to_schema)
17
+ end
18
+ end
19
+
20
+ def views_in_database
21
+ @views_in_database ||= Scenic.database.views.sort
22
+ end
23
+
24
+ private
25
+
26
+ unless ActiveRecord::SchemaDumper.instance_methods(false).include?(:ignored?)
27
+ # This method will be present in Rails 4.2.0 and can be removed then.
28
+ def ignored?(table_name)
29
+ ["schema_migrations", ignore_tables].flatten.any? do |ignored|
30
+ case ignored
31
+ when String; remove_prefix_and_suffix(table_name) == ignored
32
+ when Regexp; remove_prefix_and_suffix(table_name) =~ ignored
33
+ else
34
+ raise StandardError, "ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values."
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,84 @@
1
+ module Scenic
2
+ module Statements
3
+ # Public: Create a new database view.
4
+ #
5
+ # name - A string or symbol containing the singular name of the database
6
+ # view. Cannot conflict with any other view or table names.
7
+ # version - The version number of the view. If present, will be used to find
8
+ # the definition file in db/views in the form db/views/[pluralized
9
+ # name]_v[2 digit zero padded version].sql.
10
+ # Example: db/views/searches_v02.sql.
11
+ # sql_definition - A string containing the SQL definition of the view. If
12
+ # both sql_definition and version are provided,
13
+ # sql_definition takes prescedence.
14
+ #
15
+ # Examples
16
+ #
17
+ # create_view(:searches, version: 2)
18
+ #
19
+ # create_view(:active_users, sql_definition: <<-SQL)
20
+ # SELECT * FROM users WHERE users.active = 't'
21
+ # SQL
22
+ #
23
+ # Returns the database response from executing the CREATE VIEW statement.
24
+ def create_view(name, version: 1, sql_definition: nil)
25
+ if version.blank? && sql_definition.nil?
26
+ raise(
27
+ ArgumentError,
28
+ "view_definition or version_number must be specified"
29
+ )
30
+ end
31
+
32
+ sql_definition ||= definition(name, version)
33
+
34
+ Scenic.database.create_view(name, sql_definition)
35
+ end
36
+
37
+ # Public: Drop a database view by name.
38
+ #
39
+ # name - A string or symbol containing the singular name of the database
40
+ # view. Must be an existing view.
41
+ # revert_to_version - Used to revert the drop_view command in the
42
+ # db:rollback rake task, which would pass the version
43
+ # number to create_view. Usually the most recent
44
+ # version.
45
+ #
46
+ # Example
47
+ #
48
+ # drop_view(:users_who_recently_logged_in, 3)
49
+ #
50
+ # Returns the database response from executing the DROP VIEW statement.
51
+ def drop_view(name, revert_to_version: nil)
52
+ Scenic.database.drop_view(name)
53
+ end
54
+
55
+ # Public: Update a database view to a new version by first dropping the
56
+ # previous version then creating the new version.
57
+ #
58
+ # name - A string or symbol containing the singular name of the database
59
+ # view. Must be an existing view.
60
+ # version - The version number of the view. See create_view for details.
61
+ # revert_to_version - The version to revert to for db:rollback. Usually the
62
+ # previous version. See drop_view for details.
63
+ #
64
+ # Example
65
+ #
66
+ # update_view(:engagement_reports, version: 3, revert_to_version: 2)
67
+ #
68
+ # Returns the database response from executing the CREATE VIEW statement.
69
+ def update_view(name, version: nil, revert_to_version: nil)
70
+ if version.blank?
71
+ raise ArgumentError, "version is required"
72
+ end
73
+
74
+ drop_view(name, revert_to_version: revert_to_version)
75
+ create_view(name, version: version)
76
+ end
77
+
78
+ private
79
+
80
+ def definition(name, version)
81
+ Scenic::Definition.new(name, version).to_sql
82
+ end
83
+ end
84
+ end
@@ -1,3 +1,3 @@
1
1
  module Scenic
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,24 @@
1
+ module Scenic
2
+ class View
3
+ attr_reader :name, :definition
4
+ delegate :<=>, to: :name
5
+
6
+ def initialize(view_row)
7
+ @name = view_row["viewname"]
8
+ @definition = view_row["definition"].strip
9
+ end
10
+
11
+ def ==(other)
12
+ name == other.name &&
13
+ definition == other.definition
14
+ end
15
+
16
+ def to_schema
17
+ <<-DEFINITION.strip_heredoc
18
+ create_view :#{name}, sql_definition:<<-\SQL
19
+ #{definition}
20
+ SQL
21
+ DEFINITION
22
+ end
23
+ end
24
+ end
File without changes
@@ -1,23 +1,25 @@
1
1
  require "spec_helper"
2
2
  require "generators/scenic/model/model_generator"
3
3
 
4
- describe Scenic::Generators::ModelGenerator, :generator do
5
- before do
6
- allow(Scenic::Generators::ViewGenerator).to receive(:new)
7
- .and_return(
8
- instance_double("Scenic::Generators::ViewGenerator").as_null_object
9
- )
10
- end
4
+ module Scenic::Generators
5
+ describe ModelGenerator, :generator do
6
+ before do
7
+ allow(ViewGenerator).to receive(:new)
8
+ .and_return(
9
+ instance_double("Scenic::Generators::ViewGenerator").as_null_object
10
+ )
11
+ end
11
12
 
12
- it "invokes the view generator" do
13
- run_generator ["current_customer"]
13
+ it "invokes the view generator" do
14
+ run_generator ["current_customer"]
14
15
 
15
- expect(Scenic::Generators::ViewGenerator).to have_received(:new)
16
- end
16
+ expect(ViewGenerator).to have_received(:new)
17
+ end
17
18
 
18
- it "creates a migration to create the view" do
19
- run_generator ["current_customer"]
20
- model_definition = file("app/models/current_customer.rb")
21
- expect(model_definition).to exist
19
+ it "creates a migration to create the view" do
20
+ run_generator ["current_customer"]
21
+ model_definition = file("app/models/current_customer.rb")
22
+ expect(model_definition).to exist
23
+ end
22
24
  end
23
25
  end
@@ -2,15 +2,25 @@ require "spec_helper"
2
2
  require "generators/scenic/view/view_generator"
3
3
 
4
4
  describe Scenic::Generators::ViewGenerator, :generator do
5
- it "creates a view definition file" do
5
+ it "creates view definition and migration files" do
6
+ migration = file("db/migrate/create_searches.rb")
7
+ view_definition = file("db/views/searches_v01.sql")
8
+
6
9
  run_generator ["search"]
7
- view_definition = file("db/views/searches_v1.sql")
10
+
11
+ expect(migration).to be_a_migration
8
12
  expect(view_definition).to exist
9
13
  end
10
14
 
11
- it "creates a migration to create the view" do
15
+ it "updates an existing view" do
16
+ migration = file("db/migrate/update_searches_to_version_2.rb")
17
+ view_definition = file("db/views/searches_v02.sql")
18
+ allow(Dir).to receive(:entries)
19
+ .and_return(["searches_v01.sql"])
20
+
12
21
  run_generator ["search"]
13
- migration = file("db/migrate/create_searches.rb")
22
+
14
23
  expect(migration).to be_a_migration
24
+ expect(view_definition).to exist
15
25
  end
16
26
  end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ module Scenic
4
+ module Adapters
5
+ describe Postgres, :db do
6
+ describe "create_view" do
7
+ it "successfully creates a view" do
8
+ Postgres.create_view("greetings", "SELECT text 'hi' AS greeting")
9
+
10
+ expect(Postgres.views.map(&:name)).to include("greetings")
11
+ end
12
+ end
13
+
14
+ describe "drop_view" do
15
+ it "successfully drops a view" do
16
+ Postgres.create_view("greetings", "SELECT text 'hi' AS greeting")
17
+
18
+ Postgres.drop_view("greetings")
19
+
20
+ expect(Postgres.views.map(&:name)).not_to include("greetings")
21
+ end
22
+ end
23
+
24
+ describe "views" do
25
+ it "finds views and builds Scenic::View objects" do
26
+ ActiveRecord::Base.connection.execute "CREATE VIEW greetings AS SELECT text 'hi' AS greeting"
27
+
28
+ expect(Postgres.views).to eq([
29
+ View.new(
30
+ "viewname" => "greetings",
31
+ "definition" => "SELECT 'hi'::text AS greeting;",
32
+ ),
33
+ ])
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,6 +1,6 @@
1
1
  require "spec_helper"
2
2
 
3
- module Scenic::ActiveRecord::CommandRecorder
3
+ module Scenic::CommandRecorder
4
4
  describe StatementArguments do
5
5
  describe "#view" do
6
6
  it "is the view name" do
@@ -1,18 +1,14 @@
1
1
  require "spec_helper"
2
2
 
3
- describe Scenic::ActiveRecord::CommandRecorder do
3
+ describe Scenic::CommandRecorder do
4
4
  describe "#create_view" do
5
5
  it "records the created view" do
6
- recorder = ActiveRecord::Migration::CommandRecorder.new
7
-
8
6
  recorder.create_view :greetings
9
7
 
10
8
  expect(recorder.commands).to eq [[:create_view, [:greetings], nil]]
11
9
  end
12
10
 
13
11
  it "reverts to drop_view" do
14
- recorder = ActiveRecord::Migration::CommandRecorder.new
15
-
16
12
  recorder.revert { recorder.create_view :greetings }
17
13
 
18
14
  expect(recorder.commands).to eq [[:drop_view, [:greetings]]]
@@ -21,15 +17,12 @@ describe Scenic::ActiveRecord::CommandRecorder do
21
17
 
22
18
  describe "#drop_view" do
23
19
  it "records the dropped view" do
24
- recorder = ActiveRecord::Migration::CommandRecorder.new
25
-
26
20
  recorder.drop_view :users
27
21
 
28
22
  expect(recorder.commands).to eq [[:drop_view, [:users], nil]]
29
23
  end
30
24
 
31
25
  it "reverts to create_view with specified revert_to_version" do
32
- recorder = ActiveRecord::Migration::CommandRecorder.new
33
26
  args = [:users, { revert_to_version: 3 }]
34
27
  revert_args = [:users, { version: 3 }]
35
28
 
@@ -39,7 +32,6 @@ describe Scenic::ActiveRecord::CommandRecorder do
39
32
  end
40
33
 
41
34
  it "raises when reverting without revert_to_version set" do
42
- recorder = ActiveRecord::Migration::CommandRecorder.new
43
35
  args = [:users, { another_argument: 1 }]
44
36
 
45
37
  expect { recorder.revert { recorder.drop_view(*args) } }
@@ -49,7 +41,6 @@ describe Scenic::ActiveRecord::CommandRecorder do
49
41
 
50
42
  describe "#update_view" do
51
43
  it "records the updated view" do
52
- recorder = ActiveRecord::Migration::CommandRecorder.new
53
44
  args = [:users, { version: 2 }]
54
45
 
55
46
  recorder.update_view(*args)
@@ -58,7 +49,6 @@ describe Scenic::ActiveRecord::CommandRecorder do
58
49
  end
59
50
 
60
51
  it "reverts to update_view with the specified revert_to_version" do
61
- recorder = ActiveRecord::Migration::CommandRecorder.new
62
52
  args = [:users, { version: 2, revert_to_version: 1 }]
63
53
  revert_args = [:users, { version: 1 }]
64
54
 
@@ -68,11 +58,14 @@ describe Scenic::ActiveRecord::CommandRecorder do
68
58
  end
69
59
 
70
60
  it "raises when reverting without revert_to_version set" do
71
- recorder = ActiveRecord::Migration::CommandRecorder.new
72
61
  args = [:users, { version: 42, another_argument: 1 }]
73
62
 
74
63
  expect { recorder.revert { recorder.update_view(*args) } }
75
64
  .to raise_error(ActiveRecord::IrreversibleMigration)
76
65
  end
77
66
  end
67
+
68
+ def recorder
69
+ @recorder ||= ActiveRecord::Migration::CommandRecorder.new
70
+ end
78
71
  end
@@ -0,0 +1,56 @@
1
+ require "spec_helper"
2
+
3
+ module Scenic
4
+ describe Definition do
5
+ describe "to_sql" do
6
+ it "returns the content of a view definition" do
7
+ sql_definition = "SELECT text 'Hi' as greeting"
8
+ allow(File).to receive(:read).and_return(sql_definition)
9
+
10
+ definition = Definition.new("searches", 1)
11
+
12
+ expect(definition.to_sql).to eq sql_definition
13
+ end
14
+
15
+ it "raises an error if the file is empty" do
16
+ allow(File).to receive(:read).and_return("")
17
+
18
+ expect do
19
+ Definition.new("searches", 1).to_sql
20
+ end.to raise_error RuntimeError
21
+ end
22
+ end
23
+
24
+ describe "path" do
25
+ it "returns a sql file in db/views with padded version and view name" do
26
+ expected = "db/views/searches_v01.sql"
27
+
28
+ definition = Definition.new("searches", 1)
29
+
30
+ expect(definition.path).to eq expected
31
+ end
32
+ end
33
+
34
+ describe "full_path" do
35
+ it "joins the path with Rails.root" do
36
+ definition = Definition.new("searches", 15)
37
+
38
+ expect(definition.full_path).to eq Rails.root.join(definition.path)
39
+ end
40
+ end
41
+
42
+ describe "version" do
43
+ it "pads the version number with 0" do
44
+ definition = Definition.new(:_, 1)
45
+
46
+ expect(definition.version).to eq "01"
47
+ end
48
+
49
+ it "doesn't pad more than 2 characters" do
50
+ definition = Definition.new(:_, 15)
51
+
52
+ expect(definition.version).to eq "15"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -2,7 +2,7 @@ require "spec_helper"
2
2
 
3
3
  class Search < ActiveRecord::Base; end
4
4
 
5
- describe Scenic::ActiveRecord::SchemaDumper, :db do
5
+ describe Scenic::SchemaDumper, :db do
6
6
  it "dumps a create_view for a view in the database" do
7
7
  view_definition = "SELECT 'needle'::text AS haystack"
8
8
  Search.connection.create_view :searches, sql_definition: view_definition
@@ -0,0 +1,72 @@
1
+ require "spec_helper"
2
+
3
+ module Scenic
4
+ describe Scenic::Statements do
5
+ before do
6
+ allow(Scenic).to receive(:database)
7
+ .and_return(class_double("Scenic::Adapters::Postgres").as_null_object)
8
+ end
9
+
10
+ describe "create_view" do
11
+ it "creates a view from a file" do
12
+ version = 15
13
+ definition_stub = instance_double("Definition", to_sql: "foo")
14
+ allow(Definition).to receive(:new)
15
+ .with(:views, version)
16
+ .and_return(definition_stub)
17
+
18
+ connection.create_view :views, version: version
19
+
20
+ expect(Scenic.database).to have_received(:create_view)
21
+ .with(:views, definition_stub.to_sql)
22
+ end
23
+
24
+ it "creates a view from a text definition" do
25
+ sql_definition = "a defintion"
26
+
27
+ connection.create_view(:views, sql_definition: sql_definition)
28
+
29
+ expect(Scenic.database).to have_received(:create_view)
30
+ .with(:views, sql_definition)
31
+ end
32
+
33
+ it "raises an error if neither version nor sql_defintion are provided" do
34
+ expect do
35
+ connection.create_view :foo, version: nil, sql_definition: nil
36
+ end.to raise_error ArgumentError
37
+ end
38
+ end
39
+
40
+ describe "drop_view" do
41
+ it "removes a view from the database" do
42
+ connection.drop_view :name
43
+
44
+ expect(Scenic.database).to have_received(:drop_view).with(:name)
45
+ end
46
+ end
47
+
48
+ describe "update_view" do
49
+ it "drops the existing version and creates the new" do
50
+ definition = instance_double("Definition", to_sql: "definition")
51
+ allow(Definition).to receive(:new)
52
+ .with(:name, 3)
53
+ .and_return(definition)
54
+
55
+ connection.update_view(:name, version: 3)
56
+
57
+ expect(Scenic.database).to have_received(:drop_view).with(:name)
58
+ expect(Scenic.database).to have_received(:create_view)
59
+ .with(:name, definition.to_sql)
60
+ end
61
+
62
+ it "raises an error if not supplied a version" do
63
+ expect { connection.update_view :views }
64
+ .to raise_error(ArgumentError, /version is required/)
65
+ end
66
+ end
67
+
68
+ def connection
69
+ Class.new { extend Statements }
70
+ end
71
+ end
72
+ end
data/spec/smoke ADDED
@@ -0,0 +1,86 @@
1
+ #!/bin/bash
2
+
3
+ set -euo pipefail
4
+
5
+ setup() {
6
+ cd spec/dummy
7
+ git init
8
+ git add -A
9
+ git commit --no-gpg-sign --message "initial"
10
+ }
11
+
12
+ teardown() {
13
+ git add -A
14
+ git reset --hard HEAD
15
+ rm -rf .git/
16
+ rake db:drop db:create
17
+ }
18
+
19
+ trap teardown EXIT
20
+
21
+ verifySearchResults() {
22
+ echo "verify search results"
23
+ local expectedResult=$1
24
+ local actualResult=$(rails runner "puts Search.first.results")
25
+ [[ "$actualResult" == "$expectedResult" ]] || exit 1
26
+ echo "[success]"
27
+ }
28
+
29
+ writeToFileAndMigrateAndVerifySearchResults() {
30
+ echo "write search definition and migrate"
31
+ local version=$1
32
+ local expectedResult=$2
33
+ echo "SELECT '$expectedResult'::text AS results" >> db/views/searches_v$version\.sql
34
+ rake db:migrate
35
+ echo "[success]"
36
+ verifySearchResults $expectedResult
37
+ }
38
+
39
+ main() {
40
+ setup
41
+ echo "rails generate scenic:model search"
42
+ rails generate scenic:model search
43
+ [[ -f db/views/searches_v01.sql ]] || exit 1
44
+ [[ -f app/models/search.rb ]] || exit 1
45
+ [[ -n "$(find db/migrate -maxdepth 1 -name "*create_searches.rb" -print -quit)" ]] || exit 1
46
+ echo "[success]"
47
+
48
+ writeToFileAndMigrateAndVerifySearchResults "01" "search-results"
49
+
50
+ echo "rails generate scenic:view search (to get updates search view)"
51
+ rails generate scenic:view search
52
+ [[ -f db/views/searches_v02.sql ]] || exit 1
53
+ [[ -n "$(find db/migrate -maxdepth 1 -name "*update_searches_to_version_2.rb" -print -quit)" ]] || exit 1
54
+ echo "[success]"
55
+
56
+ writeToFileAndMigrateAndVerifySearchResults "02" "different-results"
57
+
58
+ echo "rake db:rollback"
59
+ rake db:rollback
60
+ echo "[success]"
61
+
62
+ verifySearchResults "search-results"
63
+
64
+ echo "rails destroy scenic:view search"
65
+ rails destroy scenic:view search
66
+ [[ ! -f db/views/searches_v02.sql ]] || exit 1
67
+ [[ -z "$(find db/migrate -maxdepth 1 -name "*update_searches_to_version_2.rb" -print -quit)" ]] || exit 1
68
+ [[ -f db/views/searches_v01.sql ]] || exit 1
69
+ [[ -f app/models/search.rb ]] || exit 1
70
+ [[ -n "$(find db/migrate -maxdepth 1 -name "*create_searches.rb" -print -quit)" ]] || exit 1
71
+ echo "[success]"
72
+
73
+ echo "rake db:rollback"
74
+ rake db:rollback
75
+ echo "[success]"
76
+
77
+ echo "rails destroy scenic:view search"
78
+ rails destroy scenic:model search
79
+ [[ ! -f db/views/searches_v01.sql ]] || exit 1
80
+ [[ ! -f app/models/search.rb ]] || exit 1
81
+ [[ -z "$(find db/migrate -maxdepth 1 -name "*create_searches.rb" -print -quit)" ]] || exit 1
82
+ echo "[success]"
83
+ echo "[done]"
84
+ }
85
+
86
+ main $*
@@ -1,9 +1,9 @@
1
1
  module ViewDefinitionHelpers
2
2
  def with_view_definition(name, version, schema)
3
- view_file = ::Rails.root.join("db", "views", "#{name}_v#{version}.sql")
4
- File.open(view_file, "w") { |f| f.write(schema) }
3
+ definition = Scenic::Definition.new(name, version)
4
+ File.open(definition.full_path, "w") { |f| f.write(schema) }
5
5
  yield
6
6
  ensure
7
- File.delete view_file
7
+ File.delete definition.full_path
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scenic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Prior
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-08-04 00:00:00.000000000 Z
12
+ date: 2014-08-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -152,6 +152,7 @@ files:
152
152
  - ".travis.yml"
153
153
  - Gemfile
154
154
  - LICENSE.txt
155
+ - NEWS.md
155
156
  - README.md
156
157
  - Rakefile
157
158
  - lib/generators/scenic/model/USAGE
@@ -159,14 +160,18 @@ files:
159
160
  - lib/generators/scenic/model/templates/model.erb
160
161
  - lib/generators/scenic/view/USAGE
161
162
  - lib/generators/scenic/view/templates/db/migrate/create_view.erb
163
+ - lib/generators/scenic/view/templates/db/migrate/update_view.erb
162
164
  - lib/generators/scenic/view/view_generator.rb
163
165
  - lib/scenic.rb
164
- - lib/scenic/active_record/command_recorder.rb
165
- - lib/scenic/active_record/command_recorder/statement_arguments.rb
166
- - lib/scenic/active_record/schema_dumper.rb
167
- - lib/scenic/active_record/statements.rb
166
+ - lib/scenic/adapters/postgres.rb
167
+ - lib/scenic/command_recorder.rb
168
+ - lib/scenic/command_recorder/statement_arguments.rb
169
+ - lib/scenic/definition.rb
168
170
  - lib/scenic/railtie.rb
171
+ - lib/scenic/schema_dumper.rb
172
+ - lib/scenic/statements.rb
169
173
  - lib/scenic/version.rb
174
+ - lib/scenic/view.rb
170
175
  - scenic.gemspec
171
176
  - spec/dummy/.gitignore
172
177
  - spec/dummy/Rakefile
@@ -180,14 +185,18 @@ files:
180
185
  - spec/dummy/config/environment.rb
181
186
  - spec/dummy/config/environments/development.rb
182
187
  - spec/dummy/config/environments/test.rb
188
+ - spec/dummy/db/migrate/.keep
183
189
  - spec/dummy/db/views/.keep
184
190
  - spec/generators/scenic/model/model_generator_spec.rb
185
191
  - spec/generators/scenic/view/view_generator_spec.rb
186
192
  - spec/integration/revert_spec.rb
187
- - spec/scenic/active_record/command_recorder/statement_arguments_spec.rb
188
- - spec/scenic/active_record/command_recorder_spec.rb
189
- - spec/scenic/active_record/schema_dumper_spec.rb
190
- - spec/scenic/active_record/statements_spec.rb
193
+ - spec/scenic/adapters/postgres_spec.rb
194
+ - spec/scenic/command_recorder/statement_arguments_spec.rb
195
+ - spec/scenic/command_recorder_spec.rb
196
+ - spec/scenic/definition_spec.rb
197
+ - spec/scenic/schema_dumper_spec.rb
198
+ - spec/scenic/statements_spec.rb
199
+ - spec/smoke
191
200
  - spec/spec_helper.rb
192
201
  - spec/support/generator_spec_setup.rb
193
202
  - spec/support/view_definition_helpers.rb
@@ -228,14 +237,18 @@ test_files:
228
237
  - spec/dummy/config/environment.rb
229
238
  - spec/dummy/config/environments/development.rb
230
239
  - spec/dummy/config/environments/test.rb
240
+ - spec/dummy/db/migrate/.keep
231
241
  - spec/dummy/db/views/.keep
232
242
  - spec/generators/scenic/model/model_generator_spec.rb
233
243
  - spec/generators/scenic/view/view_generator_spec.rb
234
244
  - spec/integration/revert_spec.rb
235
- - spec/scenic/active_record/command_recorder/statement_arguments_spec.rb
236
- - spec/scenic/active_record/command_recorder_spec.rb
237
- - spec/scenic/active_record/schema_dumper_spec.rb
238
- - spec/scenic/active_record/statements_spec.rb
245
+ - spec/scenic/adapters/postgres_spec.rb
246
+ - spec/scenic/command_recorder/statement_arguments_spec.rb
247
+ - spec/scenic/command_recorder_spec.rb
248
+ - spec/scenic/definition_spec.rb
249
+ - spec/scenic/schema_dumper_spec.rb
250
+ - spec/scenic/statements_spec.rb
251
+ - spec/smoke
239
252
  - spec/spec_helper.rb
240
253
  - spec/support/generator_spec_setup.rb
241
254
  - spec/support/view_definition_helpers.rb
@@ -1,44 +0,0 @@
1
- require "scenic/active_record/command_recorder/statement_arguments"
2
-
3
- module Scenic
4
- module ActiveRecord
5
- module CommandRecorder
6
- def create_view(*args)
7
- record(:create_view, args)
8
- end
9
-
10
- def drop_view(*args)
11
- record(:drop_view, args)
12
- end
13
-
14
- def update_view(*args)
15
- record(:update_view, args)
16
- end
17
-
18
- def invert_create_view(args)
19
- [:drop_view, args]
20
- end
21
-
22
- def invert_drop_view(args)
23
- perform_scenic_inversion(:create_view, args)
24
- end
25
-
26
- def invert_update_view(args)
27
- perform_scenic_inversion(:update_view, args)
28
- end
29
-
30
- private
31
-
32
- def perform_scenic_inversion(method, args)
33
- scenic_args = StatementArguments.new(args)
34
-
35
- if scenic_args.revert_to_version.nil?
36
- message = "#{method} is reversible only if given a revert_to_version"
37
- raise ::ActiveRecord::IrreversibleMigration, message
38
- end
39
-
40
- [method, scenic_args.invert_version.to_a]
41
- end
42
- end
43
- end
44
- end
@@ -1,44 +0,0 @@
1
- module Scenic
2
- module ActiveRecord
3
- module CommandRecorder
4
- class StatementArguments
5
- def initialize(args)
6
- @args = args.freeze
7
- end
8
-
9
- def view
10
- @args[0]
11
- end
12
-
13
- def version
14
- options[:version]
15
- end
16
-
17
- def revert_to_version
18
- options[:revert_to_version]
19
- end
20
-
21
- def invert_version
22
- StatementArguments.new([view, options_for_revert])
23
- end
24
-
25
- def to_a
26
- @args.to_a
27
- end
28
-
29
- private
30
-
31
- def options
32
- @options ||= @args[1] || {}
33
- end
34
-
35
- def options_for_revert
36
- options.clone.tap do |revert_options|
37
- revert_options[:version] = revert_to_version
38
- revert_options.delete(:revert_to_version)
39
- end
40
- end
41
- end
42
- end
43
- end
44
- end
@@ -1,52 +0,0 @@
1
- require "rails"
2
-
3
- module Scenic
4
- module ActiveRecord
5
- module SchemaDumper
6
- extend ActiveSupport::Concern
7
-
8
- included { alias_method_chain :tables, :views }
9
-
10
- def tables_with_views(stream)
11
- tables_without_views(stream)
12
- views(stream)
13
- end
14
-
15
- def views(stream)
16
- defined_views.sort.each do |view_name|
17
- next if ["schema_migrations", ignore_tables].flatten.any? do |ignored|
18
- case ignored
19
- when String; remove_prefix_and_suffix(view_name) == ignored
20
- when Regexp; remove_prefix_and_suffix(view_name) =~ ignored
21
- else
22
- raise StandardError, "ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values."
23
- end
24
- end
25
- view(view_name, stream)
26
- end
27
- end
28
-
29
- def view(name, stream)
30
- stream.puts(<<-DEFINITION)
31
- create_view :#{name}, sql_definition:<<-\SQL
32
- #{views_with_definitions[name]}
33
- SQL
34
- DEFINITION
35
- stream
36
- end
37
-
38
- def defined_views
39
- views_with_definitions.keys
40
- end
41
-
42
- def views_with_definitions
43
- @views_with_definitions ||=
44
- Hash[@connection.execute(<<-SQL, 'SCHEMA').values]
45
- SELECT viewname, definition
46
- FROM pg_views
47
- WHERE schemaname = ANY (current_schemas(false))
48
- SQL
49
- end
50
- end
51
- end
52
- end
@@ -1,37 +0,0 @@
1
- module Scenic
2
- module ActiveRecord
3
- module Statements
4
- def create_view(name, version: 1, sql_definition: nil)
5
- if version.nil? && sql_definition.nil?
6
- raise(
7
- ArgumentError,
8
- "view_definition or version_number must be specified"
9
- )
10
- end
11
-
12
- sql_definition ||= definition(name, version)
13
-
14
- execute "CREATE VIEW #{name} AS #{sql_definition};"
15
- end
16
-
17
- def drop_view(name, revert_to_version: nil)
18
- execute "DROP VIEW #{name};"
19
- end
20
-
21
- def update_view(name, version: nil, revert_to_version: nil)
22
- if version.nil?
23
- raise ArgumentError, "version is required"
24
- end
25
-
26
- drop_view(name)
27
- create_view(name, version: version)
28
- end
29
-
30
- private
31
-
32
- def definition(name, version)
33
- File.read(::Rails.root.join("db", "views", "#{name}_v#{version}.sql"))
34
- end
35
- end
36
- end
37
- end
@@ -1,82 +0,0 @@
1
- require "spec_helper"
2
-
3
- class View < ActiveRecord::Base
4
- end
5
-
6
- describe Scenic::ActiveRecord::Statements, :db do
7
- describe "create_view" do
8
- it "creates a view from a file" do
9
- with_view_definition :views, 1, "SELECT text 'Hello World' AS hello" do
10
- View.connection.create_view :views
11
-
12
- expect(View.all.pluck(:hello)).to eq ["Hello World"]
13
- end
14
- end
15
-
16
- it "creates a view from a text definition" do
17
- View.connection.create_view :views,
18
- sql_definition: "SELECT text 'Goodbye' AS hello"
19
-
20
- expect(View.all.pluck(:hello)).to eq ["Goodbye"]
21
- end
22
-
23
- it "creates a view from a specific version" do
24
- with_view_definition :views, 15, "SELECT text 'Hello Earth East 15' AS hello" do
25
- View.connection.create_view :views, version: 15
26
-
27
- expect(View.all.pluck(:hello)).to eq ["Hello Earth East 15"]
28
- end
29
- end
30
-
31
- it "raises an error if both arguments are nil" do
32
- expect do
33
- View.connection.create_view :whatever,
34
- version: nil,
35
- sql_definition: nil
36
- end.to raise_error ArgumentError
37
- end
38
- end
39
-
40
- describe "drop_view" do
41
- it "removes a view from the database" do
42
- with_view_definition :things, 1, "SELECT text 'Hi' AS greeting" do
43
- View.connection.create_view :things
44
-
45
- View.connection.drop_view :things
46
-
47
- expect(views).not_to include "things"
48
- end
49
- end
50
- end
51
-
52
- describe "update_view" do
53
- it "updates an existing view in the database" do
54
- with_view_definition :views, 1, "SELECT text 'Hi' AS greeting" do
55
- View.connection.create_view :views
56
- with_view_definition :views, 2, "SELECT text 'Hello' AS greeting" do
57
- View.connection.update_view :views, version: 2
58
-
59
- expect(View.all.pluck(:greeting)).to eq ['Hello']
60
- end
61
- end
62
- end
63
-
64
- it "raises an error if not supplied a version" do
65
- expect { View.connection.update_view :views }
66
- .to raise_error(ArgumentError, /version is required/)
67
- end
68
-
69
- it "raises an error if the view to be updated does not exist" do
70
- with_view_definition :views, 2, "SELECT text 'Hi' as greeting" do
71
- expect { View.connection.update_view :views, version: 2 }
72
- .to raise_error(ActiveRecord::StatementInvalid, /does not exist/)
73
- end
74
- end
75
- end
76
-
77
- def views
78
- ActiveRecord::Base.connection
79
- .execute("SELECT table_name FROM INFORMATION_SCHEMA.views")
80
- .map(&:values).flatten
81
- end
82
- end