scenic 0.1.0 → 0.2.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 (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