scenic 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.hound.yml +2 -0
  4. data/.travis.yml +17 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +96 -0
  8. data/Rakefile +6 -0
  9. data/lib/generators/scenic/model/USAGE +10 -0
  10. data/lib/generators/scenic/model/model_generator.rb +20 -0
  11. data/lib/generators/scenic/model/templates/model.erb +2 -0
  12. data/lib/generators/scenic/view/USAGE +9 -0
  13. data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +5 -0
  14. data/lib/generators/scenic/view/view_generator.rb +26 -0
  15. data/lib/scenic/active_record/command_recorder/statement_arguments.rb +44 -0
  16. data/lib/scenic/active_record/command_recorder.rb +44 -0
  17. data/lib/scenic/active_record/schema_dumper.rb +52 -0
  18. data/lib/scenic/active_record/statements.rb +37 -0
  19. data/lib/scenic/railtie.rb +11 -0
  20. data/lib/scenic/version.rb +3 -0
  21. data/lib/scenic.rb +21 -0
  22. data/scenic.gemspec +35 -0
  23. data/spec/dummy/.gitignore +16 -0
  24. data/spec/dummy/Rakefile +6 -0
  25. data/spec/dummy/bin/bundle +3 -0
  26. data/spec/dummy/bin/rails +4 -0
  27. data/spec/dummy/bin/rake +4 -0
  28. data/spec/dummy/config/application.rb +12 -0
  29. data/spec/dummy/config/boot.rb +5 -0
  30. data/spec/dummy/config/database.yml +9 -0
  31. data/spec/dummy/config/environment.rb +5 -0
  32. data/spec/dummy/config/environments/development.rb +6 -0
  33. data/spec/dummy/config/environments/test.rb +5 -0
  34. data/spec/dummy/config.ru +4 -0
  35. data/spec/dummy/db/views/.keep +0 -0
  36. data/spec/generators/scenic/model/model_generator_spec.rb +23 -0
  37. data/spec/generators/scenic/view/view_generator_spec.rb +16 -0
  38. data/spec/integration/revert_spec.rb +66 -0
  39. data/spec/scenic/active_record/command_recorder/statement_arguments_spec.rb +41 -0
  40. data/spec/scenic/active_record/command_recorder_spec.rb +78 -0
  41. data/spec/scenic/active_record/schema_dumper_spec.rb +23 -0
  42. data/spec/scenic/active_record/statements_spec.rb +82 -0
  43. data/spec/spec_helper.rb +18 -0
  44. data/spec/support/generator_spec_setup.rb +9 -0
  45. data/spec/support/view_definition_helpers.rb +9 -0
  46. metadata +241 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7f9265748cd49c4ab094b3cb7110b61e0bef6b35
4
+ data.tar.gz: c3bd33b0d1ee11d1d807b1a12046839bb8fce53c
5
+ SHA512:
6
+ metadata.gz: 6dd62a29b6991a7c21283764d64c38851ca550a1fa690d8496ee56a6fd0448c81f66c9aaa78e2c9ebabcc430ea1be2b0213f5b7315e05e2c55d4911f5cfb9115
7
+ data.tar.gz: 8248642a38d1c583b6883af55a186bea5d2926faa8dcf790996c082ad8d110da74f785db50b4a33bd948fbe6f4b4ea99e7214fdd1afc4641f17d6d2f600b06ec
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.hound.yml ADDED
@@ -0,0 +1,2 @@
1
+ DotPosition:
2
+ EnforcedStyle: leading
data/.travis.yml ADDED
@@ -0,0 +1,17 @@
1
+ before_install:
2
+ - "echo '--colour' > ~/.rspec"
3
+ - "echo 'gem: --no-document' > ~/.gemrc"
4
+ before_script:
5
+ - pushd spec/dummy && bundle exec rake db:create && popd
6
+ branches:
7
+ only:
8
+ - master
9
+ cache:
10
+ - bundler
11
+ language:
12
+ - ruby
13
+ notifications:
14
+ email:
15
+ - false
16
+ rvm:
17
+ - 2.1.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in scenic.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Derek Prior
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # Scenic
2
+
3
+ ![Boston cityscape - it's scenic](http://www.california-tour.com/blog/wp-content/uploads/2011/11/skyline-boats-shutterstock-superreduced.jpg)
4
+
5
+ ## Description
6
+
7
+ Scenic adds methods to ActiveRecord::Migration to create and manage database
8
+ views in Rails.
9
+
10
+ Using Scenic, you can use the power of SQL views in your Rails application
11
+ without having to switch your schema format to SQL. Scenic also handles
12
+ versioning your views in a way that eliminates duplication across migrations. As
13
+ an added bonus, you define the structure of your view in a SQL file, meaning you
14
+ get full SQL syntax highlighting support in the editor of your choice.
15
+
16
+ ## Great, how do I create a view?
17
+
18
+ You've got this great idea for a view you'd like to call `searches`. Create a
19
+ definition file at `db/views/searches_v1.sql` which contains the query you'd
20
+ like to build your view with. Perhaps that looks something like this:
21
+
22
+ ```sql
23
+ SELECT
24
+ statuses.id AS searchable_id,
25
+ 'Status' AS searchable_type,
26
+ comments.body AS term
27
+ FROM statuses
28
+ JOIN comments ON statuses.id = comments.status_id
29
+
30
+ UNION
31
+
32
+ SELECT
33
+ statuses.id AS searchable_id,
34
+ 'Status' AS searchable_type,
35
+ statuses.body AS term
36
+ FROM statuses
37
+ ```
38
+
39
+ Generate a new migration with the following `change` method:
40
+
41
+ ```ruby
42
+ def change
43
+ create_view :searches
44
+ end
45
+ ```
46
+
47
+ Run that migration and congrats, you've got yourself a view. The migration is
48
+ reversible and it will be dumped into your `schema.rb` file.
49
+
50
+ ## Cool, but what if I need to change that view?
51
+
52
+ Add the new query to `db/views/searches_v2.sql` and generate a new migration with
53
+ the following `change` method:
54
+
55
+ ```ruby
56
+ def change
57
+ update_view :searches, version: 2, revert_to_version: 1
58
+ end
59
+ ```
60
+
61
+ When you run that migration, your view will be updated. The `revert_to_version`
62
+ option makes that migration reversible.
63
+
64
+ ## Can I use this view to back a model?
65
+
66
+ You bet!
67
+
68
+ ```ruby
69
+ class Search < ActiveRecord::Base
70
+ private
71
+
72
+ # this isn't strictly necessary, but it will prevent
73
+ # rails from calling save, which would fail anyway.
74
+ def readonly?
75
+ true
76
+ end
77
+ end
78
+ ```
79
+
80
+ ## I don't need this view anymore. Make it go away.
81
+
82
+ We give you `drop_view` too:
83
+
84
+ ```ruby
85
+ def change
86
+ drop_view :searches, revert_to_version: 2
87
+ end
88
+ ```
89
+
90
+ ## Can you make this easier?
91
+
92
+ Yeah, we're working on it. We're going to provide some generators that will take
93
+ some of the busy work of file creation away. We'll create the SQL file, the
94
+ migration, and optionally the model for you.
95
+
96
+ Check out the issue tracker for our other plans.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,10 @@
1
+ Description:
2
+ Create a new database view and ActiveRecord::Base subclass for your
3
+ application.
4
+
5
+ Examples:
6
+ rails generate scenic:model search
7
+
8
+ create: app/models/search.rb
9
+ create: db/views/searches_v1.sql
10
+ create: db/migrate/20140803191158_create_searches.rb
@@ -0,0 +1,20 @@
1
+ require "rails/generators"
2
+ require "generators/scenic/view/view_generator"
3
+
4
+ module Scenic
5
+ module Generators
6
+ class ModelGenerator < Rails::Generators::NamedBase
7
+ source_root File.expand_path("../templates", __FILE__)
8
+
9
+ check_class_collision
10
+
11
+ def create_model_file
12
+ template("model.erb", "app/models/#{file_name}.rb")
13
+ end
14
+
15
+ def invoke_view_generator
16
+ invoke "scenic:view", [singular_name]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,2 @@
1
+ class <%= class_name %> < ActiveRecord::Base
2
+ end
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Create a new database view for your application. This will create a new
3
+ view definition file and the accompanying migration.
4
+
5
+ Examples:
6
+ rails generate scenic:view searches
7
+
8
+ create: db/views/searches_v1.sql
9
+ create: db/migrate/20140803191158_create_searches.rb
@@ -0,0 +1,5 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ create_view :<%= plural_name %>
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module Scenic
5
+ module Generators
6
+ class ViewGenerator < Rails::Generators::NamedBase
7
+ include Rails::Generators::Migration
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ def create_view_definition
11
+ create_file "db/views/#{plural_name}_v1.sql"
12
+ end
13
+
14
+ def create_migration_file
15
+ migration_template(
16
+ "db/migrate/create_view.erb",
17
+ "db/migrate/create_#{plural_name}.rb"
18
+ )
19
+ end
20
+
21
+ def self.next_migration_number(dir)
22
+ ::ActiveRecord::Generators::Base.next_migration_number(dir)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,44 @@
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
@@ -0,0 +1,44 @@
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
@@ -0,0 +1,52 @@
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
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,11 @@
1
+ require "rails/railtie"
2
+
3
+ module Scenic
4
+ class Railtie < Rails::Railtie
5
+ initializer "scenic.load" do
6
+ ActiveSupport.on_load :active_record do
7
+ Scenic.load
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Scenic
2
+ VERSION = "0.1.0"
3
+ end
data/lib/scenic.rb ADDED
@@ -0,0 +1,21 @@
1
+ require "scenic/version"
2
+ require "scenic/railtie"
3
+ require "scenic/active_record/command_recorder"
4
+ require "scenic/active_record/schema_dumper"
5
+ require "scenic/active_record/statements"
6
+
7
+ module Scenic
8
+ 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
16
+
17
+ ::ActiveRecord::SchemaDumper.class_eval do
18
+ include Scenic::ActiveRecord::SchemaDumper
19
+ end
20
+ end
21
+ end
data/scenic.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'scenic/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'scenic'
8
+ spec.version = Scenic::VERSION
9
+ spec.authors = ['Derek Prior', 'Caleb Thompson']
10
+ spec.email = ['derekprior@gmail.com', 'caleb@calebthompson.io']
11
+ spec.summary = %q{Support for database views in Rails migrations}
12
+ spec.description = <<-DESCRIPTION
13
+ Adds methods to ActiveRecord::Migration to create and manage database views
14
+ in Rails
15
+ DESCRIPTION
16
+ spec.homepage = 'https://github.com/thoughtbot/scenic'
17
+ spec.license = 'MIT'
18
+
19
+ spec.files = `git ls-files -z`.split("\x0")
20
+ spec.test_files = spec.files.grep(%r{^spec/})
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'bundler', '>= 1.5'
24
+ spec.add_development_dependency 'database_cleaner'
25
+ spec.add_development_dependency 'rake'
26
+ spec.add_development_dependency 'rspec', '~> 3.0'
27
+ spec.add_development_dependency 'pg'
28
+ spec.add_development_dependency 'pry'
29
+ spec.add_development_dependency 'ammeter'
30
+
31
+ spec.add_dependency 'activerecord', '>= 4.0.0'
32
+ spec.add_dependency 'railties', '>= 4.0.0'
33
+
34
+ spec.required_ruby_version = '~> 2.0'
35
+ end
@@ -0,0 +1,16 @@
1
+ # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2
+ #
3
+ # If you find yourself ignoring temporary files generated by your text editor
4
+ # or operating system, you probably want to add a global ignore instead:
5
+ # git config --global core.excludesfile '~/.gitignore_global'
6
+
7
+ # Ignore bundler config.
8
+ /.bundle
9
+
10
+ # Ignore the default SQLite database.
11
+ /db/*.sqlite3
12
+ /db/*.sqlite3-journal
13
+
14
+ # Ignore all logfiles and tempfiles.
15
+ /log/*.log
16
+ /tmp
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
+ load Gem.bin_path('bundler', 'bundle')
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
3
+ require_relative '../config/boot'
4
+ require 'rails/commands'
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../config/boot'
3
+ require 'rake'
4
+ Rake.application.run
@@ -0,0 +1,12 @@
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 "scenic"
8
+
9
+ module Dummy
10
+ class Application < Rails::Application
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ # Set up gems listed in the Gemfile.
2
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../../Gemfile", __FILE__)
3
+
4
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
5
+ $LOAD_PATH.unshift File.expand_path("../../../../lib", __FILE__)
@@ -0,0 +1,9 @@
1
+ development: &default
2
+ adapter: postgresql
3
+ database: dummy_development
4
+ encoding: unicode
5
+ pool: 5
6
+
7
+ test:
8
+ <<: *default
9
+ database: dummy_test
@@ -0,0 +1,5 @@
1
+ # Load the Rails application.
2
+ require File.expand_path("../application", __FILE__)
3
+
4
+ # Initialize the Rails application.
5
+ Rails.application.initialize!
@@ -0,0 +1,6 @@
1
+ Rails.application.configure do
2
+ config.cache_classes = false
3
+
4
+ # Do not eager load code on boot.
5
+ config.eager_load = false
6
+ end
@@ -0,0 +1,5 @@
1
+ Rails.application.configure do
2
+ config.cache_classes = true
3
+ config.eager_load = false
4
+ config.active_support.deprecation = :stderr
5
+ end
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Rails.application
File without changes
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+ require "generators/scenic/model/model_generator"
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
11
+
12
+ it "invokes the view generator" do
13
+ run_generator ["current_customer"]
14
+
15
+ expect(Scenic::Generators::ViewGenerator).to have_received(:new)
16
+ end
17
+
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
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ require "spec_helper"
2
+ require "generators/scenic/view/view_generator"
3
+
4
+ describe Scenic::Generators::ViewGenerator, :generator do
5
+ it "creates a view definition file" do
6
+ run_generator ["search"]
7
+ view_definition = file("db/views/searches_v1.sql")
8
+ expect(view_definition).to exist
9
+ end
10
+
11
+ it "creates a migration to create the view" do
12
+ run_generator ["search"]
13
+ migration = file("db/migrate/create_searches.rb")
14
+ expect(migration).to be_a_migration
15
+ end
16
+ end
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+
3
+ describe "Reverting scenic schema statements", :db do
4
+ around do |example|
5
+ with_view_definition :greetings, 1, "SELECT text 'hola' AS greeting" do
6
+ example.run
7
+ end
8
+ end
9
+
10
+ it "reverts dropped view to specified version" do
11
+ run_migration(migration_for_create, :up)
12
+ run_migration(migration_for_drop, :up)
13
+ run_migration(migration_for_drop, :down)
14
+
15
+ expect { execute("SELECT * from greetings") }
16
+ .not_to raise_error
17
+ end
18
+
19
+ it "reverts updated view to specified version" do
20
+ with_view_definition :greetings, 2, "SELECT text 'good day' AS greeting" do
21
+ run_migration(migration_for_create, :up)
22
+ run_migration(migration_for_update, :up)
23
+ run_migration(migration_for_update, :down)
24
+
25
+ greeting = execute("SELECT * from greetings")[0]["greeting"]
26
+
27
+ expect(greeting).to eq "hola"
28
+ end
29
+ end
30
+
31
+ def migration_for_create
32
+ Class.new(::ActiveRecord::Migration) do
33
+ def change
34
+ create_view :greetings
35
+ end
36
+ end
37
+ end
38
+
39
+ def migration_for_drop
40
+ Class.new(::ActiveRecord::Migration) do
41
+ def change
42
+ drop_view :greetings, revert_to_version: 1
43
+ end
44
+ end
45
+ end
46
+
47
+ def migration_for_update
48
+ Class.new(::ActiveRecord::Migration) do
49
+ def change
50
+ update_view :greetings, version: 2, revert_to_version: 1
51
+ end
52
+ end
53
+ end
54
+
55
+ def run_migration(migration, directions)
56
+ silence_stream(STDOUT) do
57
+ Array.wrap(directions).each do |direction|
58
+ migration.migrate(direction)
59
+ end
60
+ end
61
+ end
62
+
63
+ def execute(sql)
64
+ ActiveRecord::Base.connection.execute(sql)
65
+ end
66
+ end
@@ -0,0 +1,41 @@
1
+ require "spec_helper"
2
+
3
+ module Scenic::ActiveRecord::CommandRecorder
4
+ describe StatementArguments do
5
+ describe "#view" do
6
+ it "is the view name" do
7
+ raw_args = [:spaceships, { foo: :bar }]
8
+ args = StatementArguments.new(raw_args)
9
+
10
+ expect(args.view).to eq :spaceships
11
+ end
12
+ end
13
+
14
+ describe "#revert_to_version" do
15
+ it "is the revert_to_version from the keyword arguments" do
16
+ raw_args = [:spaceships, { revert_to_version: 42 }]
17
+ args = StatementArguments.new(raw_args)
18
+
19
+ expect(args.revert_to_version).to eq 42
20
+ end
21
+
22
+ it "is nil if the revert_to_version was not supplied" do
23
+ raw_args = [:spaceships, { foo: :bar }]
24
+ args = StatementArguments.new(raw_args)
25
+
26
+ expect(args.revert_to_version).to be nil
27
+ end
28
+ end
29
+
30
+ describe "#invert_version" do
31
+ it "returns object with version set to revert_to_version" do
32
+ raw_args = [:meatballs, { version: 42, revert_to_version: 15 }]
33
+
34
+ inverted_args = StatementArguments.new(raw_args).invert_version
35
+
36
+ expect(inverted_args.version).to eq 15
37
+ expect(inverted_args.revert_to_version).to be nil
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,78 @@
1
+ require "spec_helper"
2
+
3
+ describe Scenic::ActiveRecord::CommandRecorder do
4
+ describe "#create_view" do
5
+ it "records the created view" do
6
+ recorder = ActiveRecord::Migration::CommandRecorder.new
7
+
8
+ recorder.create_view :greetings
9
+
10
+ expect(recorder.commands).to eq [[:create_view, [:greetings], nil]]
11
+ end
12
+
13
+ it "reverts to drop_view" do
14
+ recorder = ActiveRecord::Migration::CommandRecorder.new
15
+
16
+ recorder.revert { recorder.create_view :greetings }
17
+
18
+ expect(recorder.commands).to eq [[:drop_view, [:greetings]]]
19
+ end
20
+ end
21
+
22
+ describe "#drop_view" do
23
+ it "records the dropped view" do
24
+ recorder = ActiveRecord::Migration::CommandRecorder.new
25
+
26
+ recorder.drop_view :users
27
+
28
+ expect(recorder.commands).to eq [[:drop_view, [:users], nil]]
29
+ end
30
+
31
+ it "reverts to create_view with specified revert_to_version" do
32
+ recorder = ActiveRecord::Migration::CommandRecorder.new
33
+ args = [:users, { revert_to_version: 3 }]
34
+ revert_args = [:users, { version: 3 }]
35
+
36
+ recorder.revert { recorder.drop_view(*args) }
37
+
38
+ expect(recorder.commands).to eq [[:create_view, revert_args]]
39
+ end
40
+
41
+ it "raises when reverting without revert_to_version set" do
42
+ recorder = ActiveRecord::Migration::CommandRecorder.new
43
+ args = [:users, { another_argument: 1 }]
44
+
45
+ expect { recorder.revert { recorder.drop_view(*args) } }
46
+ .to raise_error(ActiveRecord::IrreversibleMigration)
47
+ end
48
+ end
49
+
50
+ describe "#update_view" do
51
+ it "records the updated view" do
52
+ recorder = ActiveRecord::Migration::CommandRecorder.new
53
+ args = [:users, { version: 2 }]
54
+
55
+ recorder.update_view(*args)
56
+
57
+ expect(recorder.commands).to eq [[:update_view, args, nil]]
58
+ end
59
+
60
+ it "reverts to update_view with the specified revert_to_version" do
61
+ recorder = ActiveRecord::Migration::CommandRecorder.new
62
+ args = [:users, { version: 2, revert_to_version: 1 }]
63
+ revert_args = [:users, { version: 1 }]
64
+
65
+ recorder.revert { recorder.update_view(*args) }
66
+
67
+ expect(recorder.commands).to eq [[:update_view, revert_args]]
68
+ end
69
+
70
+ it "raises when reverting without revert_to_version set" do
71
+ recorder = ActiveRecord::Migration::CommandRecorder.new
72
+ args = [:users, { version: 42, another_argument: 1 }]
73
+
74
+ expect { recorder.revert { recorder.update_view(*args) } }
75
+ .to raise_error(ActiveRecord::IrreversibleMigration)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+
3
+ class Search < ActiveRecord::Base; end
4
+
5
+ describe Scenic::ActiveRecord::SchemaDumper, :db do
6
+ it "dumps a create_view for a view in the database" do
7
+ view_definition = "SELECT 'needle'::text AS haystack"
8
+ Search.connection.create_view :searches, sql_definition: view_definition
9
+ stream = StringIO.new
10
+
11
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
12
+
13
+ output = stream.string
14
+ expect(output).to include "create_view :searches"
15
+ expect(output).to include view_definition
16
+
17
+ Search.connection.drop_view :searches
18
+
19
+ silence_stream(STDOUT) { eval(output) }
20
+
21
+ expect(Search.first.haystack).to eq "needle"
22
+ end
23
+ end
@@ -0,0 +1,82 @@
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
@@ -0,0 +1,18 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+ require "database_cleaner"
3
+
4
+ require File.expand_path("../dummy/config/environment", __FILE__)
5
+ require "support/generator_spec_setup"
6
+ require "support/view_definition_helpers"
7
+
8
+ RSpec.configure do |config|
9
+ config.order = "random"
10
+ config.include ViewDefinitionHelpers
11
+ DatabaseCleaner.strategy = :transaction
12
+
13
+ config.around(:each, db: true) do |example|
14
+ DatabaseCleaner.start
15
+ example.run
16
+ DatabaseCleaner.clean
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ require "ammeter/rspec/generator/example.rb"
2
+ require "ammeter/rspec/generator/matchers.rb"
3
+
4
+ RSpec.configure do |config|
5
+ config.before(:example, :generator) do
6
+ destination File.expand_path("../../../tmp", __FILE__)
7
+ prepare_destination
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module ViewDefinitionHelpers
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) }
5
+ yield
6
+ ensure
7
+ File.delete view_file
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,241 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scenic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Derek Prior
8
+ - Caleb Thompson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-08-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '1.5'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '1.5'
28
+ - !ruby/object:Gem::Dependency
29
+ name: database_cleaner
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '3.0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '3.0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: pg
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: pry
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: ammeter
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: activerecord
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: 4.0.0
119
+ type: :runtime
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: 4.0.0
126
+ - !ruby/object:Gem::Dependency
127
+ name: railties
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: 4.0.0
133
+ type: :runtime
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: 4.0.0
140
+ description: |2
141
+ Adds methods to ActiveRecord::Migration to create and manage database views
142
+ in Rails
143
+ email:
144
+ - derekprior@gmail.com
145
+ - caleb@calebthompson.io
146
+ executables: []
147
+ extensions: []
148
+ extra_rdoc_files: []
149
+ files:
150
+ - ".gitignore"
151
+ - ".hound.yml"
152
+ - ".travis.yml"
153
+ - Gemfile
154
+ - LICENSE.txt
155
+ - README.md
156
+ - Rakefile
157
+ - lib/generators/scenic/model/USAGE
158
+ - lib/generators/scenic/model/model_generator.rb
159
+ - lib/generators/scenic/model/templates/model.erb
160
+ - lib/generators/scenic/view/USAGE
161
+ - lib/generators/scenic/view/templates/db/migrate/create_view.erb
162
+ - lib/generators/scenic/view/view_generator.rb
163
+ - 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
168
+ - lib/scenic/railtie.rb
169
+ - lib/scenic/version.rb
170
+ - scenic.gemspec
171
+ - spec/dummy/.gitignore
172
+ - spec/dummy/Rakefile
173
+ - spec/dummy/bin/bundle
174
+ - spec/dummy/bin/rails
175
+ - spec/dummy/bin/rake
176
+ - spec/dummy/config.ru
177
+ - spec/dummy/config/application.rb
178
+ - spec/dummy/config/boot.rb
179
+ - spec/dummy/config/database.yml
180
+ - spec/dummy/config/environment.rb
181
+ - spec/dummy/config/environments/development.rb
182
+ - spec/dummy/config/environments/test.rb
183
+ - spec/dummy/db/views/.keep
184
+ - spec/generators/scenic/model/model_generator_spec.rb
185
+ - spec/generators/scenic/view/view_generator_spec.rb
186
+ - 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
191
+ - spec/spec_helper.rb
192
+ - spec/support/generator_spec_setup.rb
193
+ - spec/support/view_definition_helpers.rb
194
+ homepage: https://github.com/thoughtbot/scenic
195
+ licenses:
196
+ - MIT
197
+ metadata: {}
198
+ post_install_message:
199
+ rdoc_options: []
200
+ require_paths:
201
+ - lib
202
+ required_ruby_version: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - "~>"
205
+ - !ruby/object:Gem::Version
206
+ version: '2.0'
207
+ required_rubygems_version: !ruby/object:Gem::Requirement
208
+ requirements:
209
+ - - ">="
210
+ - !ruby/object:Gem::Version
211
+ version: '0'
212
+ requirements: []
213
+ rubyforge_project:
214
+ rubygems_version: 2.2.2
215
+ signing_key:
216
+ specification_version: 4
217
+ summary: Support for database views in Rails migrations
218
+ test_files:
219
+ - spec/dummy/.gitignore
220
+ - spec/dummy/Rakefile
221
+ - spec/dummy/bin/bundle
222
+ - spec/dummy/bin/rails
223
+ - spec/dummy/bin/rake
224
+ - spec/dummy/config.ru
225
+ - spec/dummy/config/application.rb
226
+ - spec/dummy/config/boot.rb
227
+ - spec/dummy/config/database.yml
228
+ - spec/dummy/config/environment.rb
229
+ - spec/dummy/config/environments/development.rb
230
+ - spec/dummy/config/environments/test.rb
231
+ - spec/dummy/db/views/.keep
232
+ - spec/generators/scenic/model/model_generator_spec.rb
233
+ - spec/generators/scenic/view/view_generator_spec.rb
234
+ - 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
239
+ - spec/spec_helper.rb
240
+ - spec/support/generator_spec_setup.rb
241
+ - spec/support/view_definition_helpers.rb