scenic-jets 1.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +78 -0
  3. data/.gitignore +19 -0
  4. data/.hound.yml +2 -0
  5. data/.rubocop.yml +129 -0
  6. data/.yardopts +4 -0
  7. data/CHANGELOG.md +223 -0
  8. data/CODE_OF_CONDUCT.md +76 -0
  9. data/CONTRIBUTING.md +24 -0
  10. data/Gemfile +16 -0
  11. data/LICENSE.txt +22 -0
  12. data/README.md +5 -0
  13. data/Rakefile +29 -0
  14. data/SECURITY.md +14 -0
  15. data/bin/rake +17 -0
  16. data/bin/rspec +17 -0
  17. data/bin/setup +18 -0
  18. data/bin/yard +16 -0
  19. data/lib/generators/scenic/generators.rb +12 -0
  20. data/lib/generators/scenic/materializable.rb +31 -0
  21. data/lib/generators/scenic/model/USAGE +12 -0
  22. data/lib/generators/scenic/model/model_generator.rb +52 -0
  23. data/lib/generators/scenic/model/templates/model.erb +3 -0
  24. data/lib/generators/scenic/view/USAGE +20 -0
  25. data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +5 -0
  26. data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +12 -0
  27. data/lib/generators/scenic/view/view_generator.rb +127 -0
  28. data/lib/scenic.rb +31 -0
  29. data/lib/scenic/adapters/postgres.rb +256 -0
  30. data/lib/scenic/adapters/postgres/connection.rb +57 -0
  31. data/lib/scenic/adapters/postgres/errors.rb +26 -0
  32. data/lib/scenic/adapters/postgres/index_reapplication.rb +71 -0
  33. data/lib/scenic/adapters/postgres/indexes.rb +53 -0
  34. data/lib/scenic/adapters/postgres/refresh_dependencies.rb +116 -0
  35. data/lib/scenic/adapters/postgres/views.rb +74 -0
  36. data/lib/scenic/command_recorder.rb +52 -0
  37. data/lib/scenic/command_recorder/statement_arguments.rb +51 -0
  38. data/lib/scenic/configuration.rb +37 -0
  39. data/lib/scenic/definition.rb +35 -0
  40. data/lib/scenic/index.rb +36 -0
  41. data/lib/scenic/schema_dumper.rb +44 -0
  42. data/lib/scenic/statements.rb +163 -0
  43. data/lib/scenic/version.rb +3 -0
  44. data/lib/scenic/view.rb +54 -0
  45. data/scenic.gemspec +36 -0
  46. data/spec/acceptance/user_manages_views_spec.rb +88 -0
  47. data/spec/acceptance_helper.rb +33 -0
  48. data/spec/dummy/.gitignore +16 -0
  49. data/spec/dummy/Rakefile +13 -0
  50. data/spec/dummy/app/models/application_record.rb +5 -0
  51. data/spec/dummy/bin/bundle +3 -0
  52. data/spec/dummy/bin/rails +4 -0
  53. data/spec/dummy/bin/rake +4 -0
  54. data/spec/dummy/config.ru +4 -0
  55. data/spec/dummy/config/application.rb +15 -0
  56. data/spec/dummy/config/boot.rb +5 -0
  57. data/spec/dummy/config/database.yml +14 -0
  58. data/spec/dummy/config/environment.rb +5 -0
  59. data/spec/dummy/db/migrate/.keep +0 -0
  60. data/spec/dummy/db/views/.keep +0 -0
  61. data/spec/generators/scenic/model/model_generator_spec.rb +36 -0
  62. data/spec/generators/scenic/view/view_generator_spec.rb +57 -0
  63. data/spec/integration/revert_spec.rb +74 -0
  64. data/spec/scenic/adapters/postgres/connection_spec.rb +79 -0
  65. data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +82 -0
  66. data/spec/scenic/adapters/postgres/views_spec.rb +37 -0
  67. data/spec/scenic/adapters/postgres_spec.rb +209 -0
  68. data/spec/scenic/command_recorder/statement_arguments_spec.rb +41 -0
  69. data/spec/scenic/command_recorder_spec.rb +111 -0
  70. data/spec/scenic/configuration_spec.rb +27 -0
  71. data/spec/scenic/definition_spec.rb +62 -0
  72. data/spec/scenic/schema_dumper_spec.rb +115 -0
  73. data/spec/scenic/statements_spec.rb +199 -0
  74. data/spec/spec_helper.rb +22 -0
  75. data/spec/support/generator_spec_setup.rb +14 -0
  76. data/spec/support/view_definition_helpers.rb +10 -0
  77. metadata +307 -0
@@ -0,0 +1,163 @@
1
+ module Scenic
2
+ # Methods that are made available in migrations for managing Scenic views.
3
+ module Statements
4
+ # Create a new database view.
5
+ #
6
+ # @param name [String, Symbol] The name of the database view.
7
+ # @param version [Fixnum] The version number of the view, used to find the
8
+ # definition file in `db/views`. This defaults to `1` if not provided.
9
+ # @param sql_definition [String] The SQL query for the view schema. An error
10
+ # will be raised if `sql_definition` and `version` are both set,
11
+ # as they are mutually exclusive.
12
+ # @param materialized [Boolean, Hash] Set to true to create a materialized
13
+ # view. Set to { no_data: true } to create materialized view without
14
+ # loading data. Defaults to false.
15
+ # @return The database response from executing the create statement.
16
+ #
17
+ # @example Create from `db/views/searches_v02.sql`
18
+ # create_view(:searches, version: 2)
19
+ #
20
+ # @example Create from provided SQL string
21
+ # create_view(:active_users, sql_definition: <<-SQL)
22
+ # SELECT * FROM users WHERE users.active = 't'
23
+ # SQL
24
+ #
25
+ def create_view(name, version: nil, sql_definition: nil, materialized: false)
26
+ if version.present? && sql_definition.present?
27
+ raise(
28
+ ArgumentError,
29
+ "sql_definition and version cannot both be set",
30
+ )
31
+ end
32
+
33
+ if version.blank? && sql_definition.blank?
34
+ version = 1
35
+ end
36
+
37
+ sql_definition ||= definition(name, version)
38
+
39
+ if materialized
40
+ Scenic.database.create_materialized_view(
41
+ name,
42
+ sql_definition,
43
+ no_data: no_data(materialized),
44
+ )
45
+ else
46
+ Scenic.database.create_view(name, sql_definition)
47
+ end
48
+ end
49
+
50
+ # Drop a database view by name.
51
+ #
52
+ # @param name [String, Symbol] The name of the database view.
53
+ # @param revert_to_version [Fixnum] Used to reverse the `drop_view` command
54
+ # on `rake db:rollback`. The provided version will be passed as the
55
+ # `version` argument to {#create_view}.
56
+ # @param materialized [Boolean] Set to true if dropping a meterialized view.
57
+ # defaults to false.
58
+ # @return The database response from executing the drop statement.
59
+ #
60
+ # @example Drop a view, rolling back to version 3 on rollback
61
+ # drop_view(:users_who_recently_logged_in, revert_to_version: 3)
62
+ #
63
+ def drop_view(name, revert_to_version: nil, materialized: false)
64
+ if materialized
65
+ Scenic.database.drop_materialized_view(name)
66
+ else
67
+ Scenic.database.drop_view(name)
68
+ end
69
+ end
70
+
71
+ # Update a database view to a new version.
72
+ #
73
+ # The existing view is dropped and recreated using the supplied `version`
74
+ # parameter.
75
+ #
76
+ # @param name [String, Symbol] The name of the database view.
77
+ # @param version [Fixnum] The version number of the view.
78
+ # @param sql_definition [String] The SQL query for the view schema. An error
79
+ # will be raised if `sql_definition` and `version` are both set,
80
+ # as they are mutually exclusive.
81
+ # @param revert_to_version [Fixnum] The version number to rollback to on
82
+ # `rake db rollback`
83
+ # @param materialized [Boolean, Hash] True if updating a materialized view.
84
+ # Set to { no_data: true } to update materialized view without loading
85
+ # data. Defaults to false.
86
+ # @return The database response from executing the create statement.
87
+ #
88
+ # @example
89
+ # update_view :engagement_reports, version: 3, revert_to_version: 2
90
+ #
91
+ def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, materialized: false)
92
+ if version.blank? && sql_definition.blank?
93
+ raise(
94
+ ArgumentError,
95
+ "sql_definition or version must be specified",
96
+ )
97
+ end
98
+
99
+ if version.present? && sql_definition.present?
100
+ raise(
101
+ ArgumentError,
102
+ "sql_definition and version cannot both be set",
103
+ )
104
+ end
105
+
106
+ sql_definition ||= definition(name, version)
107
+
108
+ if materialized
109
+ Scenic.database.update_materialized_view(
110
+ name,
111
+ sql_definition,
112
+ no_data: no_data(materialized),
113
+ )
114
+ else
115
+ Scenic.database.update_view(name, sql_definition)
116
+ end
117
+ end
118
+
119
+ # Update a database view to a new version using `CREATE OR REPLACE VIEW`.
120
+ #
121
+ # The existing view is replaced using the supplied `version`
122
+ # parameter.
123
+ #
124
+ # Does not work with materialized views due to lack of database support.
125
+ #
126
+ # @param name [String, Symbol] The name of the database view.
127
+ # @param version [Fixnum] The version number of the view.
128
+ # @param revert_to_version [Fixnum] The version number to rollback to on
129
+ # `rake db rollback`
130
+ # @return The database response from executing the create statement.
131
+ #
132
+ # @example
133
+ # replace_view :engagement_reports, version: 3, revert_to_version: 2
134
+ #
135
+ def replace_view(name, version: nil, revert_to_version: nil, materialized: false)
136
+ if version.blank?
137
+ raise ArgumentError, "version is required"
138
+ end
139
+
140
+ if materialized
141
+ raise ArgumentError, "Cannot replace materialized views"
142
+ end
143
+
144
+ sql_definition = definition(name, version)
145
+
146
+ Scenic.database.replace_view(name, sql_definition)
147
+ end
148
+
149
+ private
150
+
151
+ def definition(name, version)
152
+ Scenic::Definition.new(name, version).to_sql
153
+ end
154
+
155
+ def no_data(materialized)
156
+ if materialized.is_a?(Hash)
157
+ materialized.fetch(:no_data, false)
158
+ else
159
+ false
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,3 @@
1
+ module Scenic
2
+ VERSION = "1.5.4".freeze
3
+ end
@@ -0,0 +1,54 @@
1
+ module Scenic
2
+ # The in-memory representation of a view definition.
3
+ #
4
+ # **This object is used internally by adapters and the schema dumper and is
5
+ # not intended to be used by application code. It is documented here for
6
+ # use by adapter gems.**
7
+ #
8
+ # @api extension
9
+ class View
10
+ # The name of the view
11
+ # @return [String]
12
+ attr_reader :name
13
+
14
+ # The SQL schema for the query that defines the view
15
+ # @return [String]
16
+ #
17
+ # @example
18
+ # "SELECT name, email FROM users UNION SELECT name, email FROM contacts"
19
+ attr_reader :definition
20
+
21
+ # True if the view is materialized
22
+ # @return [Boolean]
23
+ attr_reader :materialized
24
+
25
+ # Returns a new instance of View.
26
+ #
27
+ # @param name [String] The name of the view.
28
+ # @param definition [String] The SQL for the query that defines the view.
29
+ # @param materialized [String] `true` if the view is materialized.
30
+ def initialize(name:, definition:, materialized:)
31
+ @name = name
32
+ @definition = definition
33
+ @materialized = materialized
34
+ end
35
+
36
+ # @api private
37
+ def ==(other)
38
+ name == other.name &&
39
+ definition == other.definition &&
40
+ materialized == other.materialized
41
+ end
42
+
43
+ # @api private
44
+ def to_schema
45
+ materialized_option = materialized ? "materialized: true, " : ""
46
+
47
+ <<-DEFINITION
48
+ create_view #{name.inspect}, #{materialized_option}sql_definition: <<-\SQL
49
+ #{definition.indent(2)}
50
+ SQL
51
+ DEFINITION
52
+ end
53
+ end
54
+ end
data/scenic.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "scenic/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "scenic-jets"
7
+ spec.version = Scenic::VERSION
8
+ spec.authors = ["Derek Prior", "Caleb Hearth"]
9
+ spec.email = ["derekprior@gmail.com", "caleb@calebhearth.com"]
10
+ spec.summary = "Support for database views in Rails migrations"
11
+ spec.description = <<-DESCRIPTION
12
+ Adds methods to ActiveRecord::Migration to create and manage database views
13
+ in Rails
14
+ DESCRIPTION
15
+ spec.homepage = "https://github.com/scenic-views/scenic"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.test_files = spec.files.grep(%r{^spec/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", ">= 1.5"
23
+ spec.add_development_dependency "database_cleaner"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec", ">= 3.3"
26
+ spec.add_development_dependency "pg", "~> 0.19"
27
+ spec.add_development_dependency "pry"
28
+ spec.add_development_dependency "ammeter", ">= 1.1.3"
29
+ spec.add_development_dependency "yard"
30
+ spec.add_development_dependency "redcarpet"
31
+
32
+ spec.add_dependency "activerecord", ">= 4.0.0"
33
+ spec.add_dependency "railties", ">= 4.0.0"
34
+
35
+ spec.required_ruby_version = ">= 2.3.0"
36
+ end
@@ -0,0 +1,88 @@
1
+ require "acceptance_helper"
2
+ require "English"
3
+
4
+ describe "User manages views" do
5
+ it "handles simple views" do
6
+ successfully "rails generate scenic:model search_result"
7
+ write_definition "search_results_v01", "SELECT 'needle'::text AS term"
8
+
9
+ successfully "rake db:migrate"
10
+ verify_result "SearchResult.take.term", "needle"
11
+
12
+ successfully "rails generate scenic:view search_results"
13
+ verify_identical_view_definitions "search_results_v01", "search_results_v02"
14
+
15
+ write_definition "search_results_v02", "SELECT 'haystack'::text AS term"
16
+ successfully "rake db:migrate"
17
+
18
+ successfully "rake db:reset"
19
+ verify_result "SearchResult.take.term", "haystack"
20
+
21
+ successfully "rake db:rollback"
22
+ successfully "rake db:rollback"
23
+ successfully "rails destroy scenic:model search_result"
24
+ end
25
+
26
+ it "handles materialized views" do
27
+ successfully "rails generate scenic:model child --materialized"
28
+ write_definition "children_v01", "SELECT 'Owen'::text AS name, 5 AS age"
29
+
30
+ successfully "rake db:migrate"
31
+ verify_result "Child.take.name", "Owen"
32
+
33
+ add_index "children", "name"
34
+ add_index "children", "age"
35
+
36
+ successfully "rails runner 'Child.refresh'"
37
+
38
+ successfully "rails generate scenic:view child --materialized"
39
+ verify_identical_view_definitions "children_v01", "children_v02"
40
+
41
+ write_definition "children_v02", "SELECT 'Elliot'::text AS name"
42
+ successfully "rake db:migrate"
43
+
44
+ successfully "rake db:reset"
45
+ verify_result "Child.take.name", "Elliot"
46
+ verify_schema_contains 'add_index "children"'
47
+
48
+ successfully "rake db:rollback"
49
+ successfully "rake db:rollback"
50
+ successfully "rails destroy scenic:model child"
51
+ end
52
+
53
+ it "handles plural view names gracefully during generation" do
54
+ successfully "rails generate scenic:model search_results --materialized"
55
+ successfully "rails destroy scenic:model search_results --materialized"
56
+ end
57
+
58
+ def successfully(command)
59
+ `RAILS_ENV=test #{command}`
60
+ expect($CHILD_STATUS.exitstatus).to eq(0), "'#{command}' was unsuccessful"
61
+ end
62
+
63
+ def write_definition(file, contents)
64
+ File.open("db/views/#{file}.sql", File::WRONLY) do |definition|
65
+ definition.truncate(0)
66
+ definition.write(contents)
67
+ end
68
+ end
69
+
70
+ def verify_result(command, expected_output)
71
+ successfully %{rails runner "#{command} == '#{expected_output}' || exit(1)"}
72
+ end
73
+
74
+ def verify_identical_view_definitions(def_a, def_b)
75
+ successfully "cmp db/views/#{def_a}.sql db/views/#{def_b}.sql"
76
+ end
77
+
78
+ def add_index(table, column)
79
+ successfully(<<-CMD.strip)
80
+ rails runner 'ActiveRecord::Migration.add_index "#{table}", "#{column}"'
81
+ CMD
82
+ end
83
+
84
+ def verify_schema_contains(statement)
85
+ expect(File.readlines("db/schema.rb").grep(/#{statement}/))
86
+ .not_to be_empty, "Schema does not contain '#{statement}'"
87
+ end
88
+ end
@@ -0,0 +1,33 @@
1
+ require "bundler"
2
+
3
+ ENV["RAILS_ENV"] = "test"
4
+
5
+ RSpec.configure do |config|
6
+ config.around(:each) do |example|
7
+ Dir.chdir("spec/dummy") do
8
+ example.run
9
+ end
10
+ end
11
+
12
+ config.before(:suite) do
13
+ Dir.chdir("spec/dummy") do
14
+ system <<-CMD
15
+ git init 1>/dev/null &&
16
+ git add -A &&
17
+ git commit --no-gpg-sign --message 'initial' 1>/dev/null
18
+ CMD
19
+ end
20
+ end
21
+
22
+ config.after(:suite) do
23
+ Dir.chdir("spec/dummy") do
24
+ system <<-CMD
25
+ echo &&
26
+ rake db:environment:set db:drop db:create &&
27
+ git add -A &&
28
+ git reset --hard HEAD 1>/dev/null &&
29
+ rm -rf .git/ 1>/dev/null
30
+ CMD
31
+ end
32
+ end
33
+ 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,13 @@
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
7
+
8
+ unless Rake::Task.task_defined?('db:environment:set')
9
+ desc 'dummy task for rails versions where this task does not exist'
10
+ task 'db:environment:set' do
11
+ #no op
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ if Rails::VERSION::STRING >= "5.0.0"
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -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,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