scenic-jets 1.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +78 -0
- data/.gitignore +19 -0
- data/.hound.yml +2 -0
- data/.rubocop.yml +129 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +223 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/CONTRIBUTING.md +24 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +5 -0
- data/Rakefile +29 -0
- data/SECURITY.md +14 -0
- data/bin/rake +17 -0
- data/bin/rspec +17 -0
- data/bin/setup +18 -0
- data/bin/yard +16 -0
- data/lib/generators/scenic/generators.rb +12 -0
- data/lib/generators/scenic/materializable.rb +31 -0
- data/lib/generators/scenic/model/USAGE +12 -0
- data/lib/generators/scenic/model/model_generator.rb +52 -0
- data/lib/generators/scenic/model/templates/model.erb +3 -0
- data/lib/generators/scenic/view/USAGE +20 -0
- data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +5 -0
- data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +12 -0
- data/lib/generators/scenic/view/view_generator.rb +127 -0
- data/lib/scenic.rb +31 -0
- data/lib/scenic/adapters/postgres.rb +256 -0
- data/lib/scenic/adapters/postgres/connection.rb +57 -0
- data/lib/scenic/adapters/postgres/errors.rb +26 -0
- data/lib/scenic/adapters/postgres/index_reapplication.rb +71 -0
- data/lib/scenic/adapters/postgres/indexes.rb +53 -0
- data/lib/scenic/adapters/postgres/refresh_dependencies.rb +116 -0
- data/lib/scenic/adapters/postgres/views.rb +74 -0
- data/lib/scenic/command_recorder.rb +52 -0
- data/lib/scenic/command_recorder/statement_arguments.rb +51 -0
- data/lib/scenic/configuration.rb +37 -0
- data/lib/scenic/definition.rb +35 -0
- data/lib/scenic/index.rb +36 -0
- data/lib/scenic/schema_dumper.rb +44 -0
- data/lib/scenic/statements.rb +163 -0
- data/lib/scenic/version.rb +3 -0
- data/lib/scenic/view.rb +54 -0
- data/scenic.gemspec +36 -0
- data/spec/acceptance/user_manages_views_spec.rb +88 -0
- data/spec/acceptance_helper.rb +33 -0
- data/spec/dummy/.gitignore +16 -0
- data/spec/dummy/Rakefile +13 -0
- data/spec/dummy/app/models/application_record.rb +5 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +15 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +14 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/db/migrate/.keep +0 -0
- data/spec/dummy/db/views/.keep +0 -0
- data/spec/generators/scenic/model/model_generator_spec.rb +36 -0
- data/spec/generators/scenic/view/view_generator_spec.rb +57 -0
- data/spec/integration/revert_spec.rb +74 -0
- data/spec/scenic/adapters/postgres/connection_spec.rb +79 -0
- data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +82 -0
- data/spec/scenic/adapters/postgres/views_spec.rb +37 -0
- data/spec/scenic/adapters/postgres_spec.rb +209 -0
- data/spec/scenic/command_recorder/statement_arguments_spec.rb +41 -0
- data/spec/scenic/command_recorder_spec.rb +111 -0
- data/spec/scenic/configuration_spec.rb +27 -0
- data/spec/scenic/definition_spec.rb +62 -0
- data/spec/scenic/schema_dumper_spec.rb +115 -0
- data/spec/scenic/statements_spec.rb +199 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/generator_spec_setup.rb +14 -0
- data/spec/support/view_definition_helpers.rb +10 -0
- 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
|
data/lib/scenic/view.rb
ADDED
@@ -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
|
data/spec/dummy/Rakefile
ADDED
@@ -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
|
data/spec/dummy/bin/rake
ADDED