monocle-views 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +75 -0
- data/LICENSE.txt +21 -0
- data/README.md +109 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/monocle.rb +101 -0
- data/lib/monocle/bump_command.rb +23 -0
- data/lib/monocle/configuration.rb +16 -0
- data/lib/monocle/generators/install_generator.rb +10 -0
- data/lib/monocle/generators/matview_generator.rb +27 -0
- data/lib/monocle/generators/view_generator.rb +27 -0
- data/lib/monocle/list_command.rb +19 -0
- data/lib/monocle/migration.rb +8 -0
- data/lib/monocle/railtie.rb +11 -0
- data/lib/monocle/version.rb +3 -0
- data/lib/monocle/version_generator.rb +18 -0
- data/lib/monocle/view.rb +147 -0
- data/lib/tasks/monocle.rake +34 -0
- data/monocle.gemspec +35 -0
- metadata +219 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 71b4dae54e7413c864bc7647293313599525af68
|
4
|
+
data.tar.gz: c8b2ca16c6ead7b30f01c34ef6c9faaae7f0b02f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b4e071b3599073aa7390b8cc6e31f551b001cd6dba47568469e7a3896db7728efeb1405a256a668cc082ae24258f06367acddefc82f3272e4e5b4aafb287902a
|
7
|
+
data.tar.gz: 79c9f5e9c97f8382fde8d7448811425a1519254ea2c5f36a1d96648beb3e2dba7388ebebaf2ec0a83806cd3ac2574ba1ca2f8316b352ec52885b9bfa1bb43fa9
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.1
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
ar-monocle (0.1.6)
|
5
|
+
activerecord (>= 4, < 6)
|
6
|
+
activesupport (>= 4, < 6)
|
7
|
+
rake
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (5.1.0)
|
13
|
+
activesupport (= 5.1.0)
|
14
|
+
activerecord (5.1.0)
|
15
|
+
activemodel (= 5.1.0)
|
16
|
+
activesupport (= 5.1.0)
|
17
|
+
arel (~> 8.0)
|
18
|
+
activesupport (5.1.0)
|
19
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
20
|
+
i18n (~> 0.7)
|
21
|
+
minitest (~> 5.1)
|
22
|
+
tzinfo (~> 1.1)
|
23
|
+
arel (8.0.0)
|
24
|
+
coderay (1.1.1)
|
25
|
+
concurrent-ruby (1.0.5)
|
26
|
+
database_cleaner (1.5.3)
|
27
|
+
diff-lcs (1.3)
|
28
|
+
dotenv (2.2.0)
|
29
|
+
i18n (0.8.1)
|
30
|
+
metaclass (0.0.4)
|
31
|
+
method_source (0.8.2)
|
32
|
+
minitest (5.10.2)
|
33
|
+
mocha (1.2.1)
|
34
|
+
metaclass (~> 0.0.1)
|
35
|
+
pg (0.19.0)
|
36
|
+
pry (0.10.4)
|
37
|
+
coderay (~> 1.1.0)
|
38
|
+
method_source (~> 0.8.1)
|
39
|
+
slop (~> 3.4)
|
40
|
+
pry-nav (0.2.4)
|
41
|
+
pry (>= 0.9.10, < 0.11.0)
|
42
|
+
rake (12.0.0)
|
43
|
+
rspec (3.5.0)
|
44
|
+
rspec-core (~> 3.5.0)
|
45
|
+
rspec-expectations (~> 3.5.0)
|
46
|
+
rspec-mocks (~> 3.5.0)
|
47
|
+
rspec-core (3.5.4)
|
48
|
+
rspec-support (~> 3.5.0)
|
49
|
+
rspec-expectations (3.5.0)
|
50
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
51
|
+
rspec-support (~> 3.5.0)
|
52
|
+
rspec-mocks (3.5.0)
|
53
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
54
|
+
rspec-support (~> 3.5.0)
|
55
|
+
rspec-support (3.5.0)
|
56
|
+
slop (3.6.0)
|
57
|
+
thread_safe (0.3.6)
|
58
|
+
tzinfo (1.2.3)
|
59
|
+
thread_safe (~> 0.1)
|
60
|
+
|
61
|
+
PLATFORMS
|
62
|
+
ruby
|
63
|
+
|
64
|
+
DEPENDENCIES
|
65
|
+
ar-monocle!
|
66
|
+
bundler (~> 1.14)
|
67
|
+
database_cleaner
|
68
|
+
dotenv
|
69
|
+
mocha
|
70
|
+
pg
|
71
|
+
pry-nav
|
72
|
+
rspec (~> 3.0)
|
73
|
+
|
74
|
+
BUNDLED WITH
|
75
|
+
1.14.6
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Leonardo Bighetti
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
|
2
|
+
# Monocle
|
3
|
+
|
4
|
+
Monocle helps you tame your database views by keeping the SQLs versioned neatly in your project and knowing when and how to migrate them if necessary. It knows how to deal with PostgreSQL materialized views and dependencies (view A points to view B) as well as regular views.
|
5
|
+
|
6
|
+
Monocle works with or without Rails, all it assumes is you're using ActiveRecord. See _Usage_ for more details.
|
7
|
+
|
8
|
+
## Reasoning
|
9
|
+
|
10
|
+
At [InvitedHome](http://invitedhome.com/) we needed an easy to use system to manage a bunch of complex views (often materialized) that we use for things like caching.
|
11
|
+
|
12
|
+
The only gem that did something similar at the time was Thoughtbot's [Scenic](https://github.com/thoughtbot/scenic), but we didn't like some of its features such as how it would generate multiple versions of the same view's SQL.
|
13
|
+
|
14
|
+
We wanted something way simpler, one SQL file per view, versioning maintained by a timestamp at the top of the file. Thus, Monocle was born.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'ar-monocle', require: 'monocle'
|
22
|
+
```
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
$ bundle
|
27
|
+
|
28
|
+
Or install it yourself as:
|
29
|
+
|
30
|
+
$ gem install ar-monocle
|
31
|
+
|
32
|
+
## Setup
|
33
|
+
|
34
|
+
If you're using Rails, there are generators for bootstrapping the gem:
|
35
|
+
|
36
|
+
$ rails g monocle:install
|
37
|
+
|
38
|
+
It will generate a migration for creating the Monocle::Migration table. If you're not using Rails, you'll need to create the table yourself. Check https://github.com/darkside/monocle/blob/master/spec/support/database_utils.rb for an example on how to do it.
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
The basic gist is you have a `db/views` in your project which contains all the view / materialized view SQL definitions. On top of those files there's a timestamp that you can control. Every time you change that timestamp, Monocle will try to migrate that view when calling `rake monocle:migrate`. You can automate this easily by hooking `monocle:migrate` to your deployment process.
|
43
|
+
|
44
|
+
Monocle knows about view dependencies and will drop and recreate dependants as necessary. So if you have a view A that references a view B and you need to upgrade view B, it will drop view A first, then drop and create view B, then create view A.
|
45
|
+
|
46
|
+
## Included Generators (for Rails)
|
47
|
+
|
48
|
+
### Generating a view
|
49
|
+
|
50
|
+
With Rails, you can use the generator:
|
51
|
+
|
52
|
+
$ rails g monocle:view view_name
|
53
|
+
|
54
|
+
This will generate a Monocle SQL template and a model. You can skip creating the model with `--skip-model`.
|
55
|
+
|
56
|
+
### Generating a materialized view
|
57
|
+
|
58
|
+
With Rails, you can use the generator:
|
59
|
+
|
60
|
+
$ rails g monocle:matview view_name
|
61
|
+
|
62
|
+
This will generate a Monocle materialized SQL template and a model. You can skip creating the model with `--skip-model`.
|
63
|
+
|
64
|
+
## Included Rake Tasks
|
65
|
+
|
66
|
+
### List all views
|
67
|
+
|
68
|
+
You can use `rake monocle:list` to see all the view names that are being managed by Monocle.
|
69
|
+
|
70
|
+
### List all migrated view slugs
|
71
|
+
|
72
|
+
You can use `rake monocle:versions` to see all the view slugs that have been migrated by Monocle.
|
73
|
+
|
74
|
+
### Migrate views
|
75
|
+
|
76
|
+
You can use `rake monocle:migrate` to migrate any views that have a new timestamp. I recommend you hook this to your deployment process i.e after you call `rake db:migrate`
|
77
|
+
|
78
|
+
### Bumping a view timestamp
|
79
|
+
|
80
|
+
With monocle, you decide when it's time to upgrade a view. So even if you have an updated view definition that you're working on, it won't actually change it unless the timestamp has changed. To bump a view timestamp, you can either do it yourself by changing the first line of the template or use the supplied rake task:
|
81
|
+
|
82
|
+
$ rake monocle:bump[my_view_name]
|
83
|
+
|
84
|
+
### Refresh a view
|
85
|
+
|
86
|
+
For materialized views, this makes it easy for you to trigger a refresh, say, in a cron job or something.
|
87
|
+
|
88
|
+
$ rake monocle:refresh[my_view_name]
|
89
|
+
|
90
|
+
### Refresh all views
|
91
|
+
|
92
|
+
This is also available as a top level method for Monocle. It will refresh all your materialized views.
|
93
|
+
|
94
|
+
$ rake monocle:refresh_all
|
95
|
+
|
96
|
+
## Development
|
97
|
+
|
98
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
99
|
+
|
100
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
101
|
+
|
102
|
+
## Contributing
|
103
|
+
|
104
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/darkside/monocle.
|
105
|
+
|
106
|
+
## License
|
107
|
+
|
108
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
109
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "monocle"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/monocle.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# Bare essentials from Rails to make this work neatly
|
2
|
+
require "active_support/core_ext/module/delegation"
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
require 'monocle/configuration'
|
6
|
+
require 'monocle/railtie' if defined?(Rails)
|
7
|
+
|
8
|
+
require "monocle/version"
|
9
|
+
require "monocle/version_generator"
|
10
|
+
require "monocle/view"
|
11
|
+
require "monocle/migration"
|
12
|
+
|
13
|
+
require "monocle/bump_command"
|
14
|
+
require "monocle/list_command"
|
15
|
+
|
16
|
+
module Monocle
|
17
|
+
|
18
|
+
class << self
|
19
|
+
delegate :path_to_views, :logger, to: :configuration
|
20
|
+
|
21
|
+
def list
|
22
|
+
@list ||= ListCommand.new.call
|
23
|
+
end
|
24
|
+
|
25
|
+
def drop(view_name)
|
26
|
+
fetch(view_name).drop
|
27
|
+
end
|
28
|
+
|
29
|
+
def create(view_name)
|
30
|
+
fetch(view_name).create
|
31
|
+
end
|
32
|
+
|
33
|
+
def versions
|
34
|
+
Migration.versions
|
35
|
+
end
|
36
|
+
|
37
|
+
def migrate
|
38
|
+
logger.info "Starting materialized views migrations..."
|
39
|
+
list.each do |key, view|
|
40
|
+
logger.debug "Checking if #{key} is up to date..."
|
41
|
+
view.migrate
|
42
|
+
end
|
43
|
+
logger.info "All done!"
|
44
|
+
end
|
45
|
+
|
46
|
+
def bump(view_name)
|
47
|
+
BumpCommand.new(fetch(view_name)).call
|
48
|
+
end
|
49
|
+
|
50
|
+
def refresh(view_name, concurrently: false)
|
51
|
+
fetch(view_name).refresh concurrently: concurrently
|
52
|
+
end
|
53
|
+
|
54
|
+
def refresh_all
|
55
|
+
list.each do |key, view|
|
56
|
+
logger.info "Refreshing view #{key}..."
|
57
|
+
view.refresh # this will be a noop for non matviews
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Enables you to configure things in a block, i.e
|
62
|
+
# Monocle.configure do |config|
|
63
|
+
# config.logger = MyLogger.new
|
64
|
+
# config.path_to_views = "my/different/path/to/my/sql/files"
|
65
|
+
# end
|
66
|
+
def configure
|
67
|
+
yield configuration if block_given?
|
68
|
+
end
|
69
|
+
|
70
|
+
def views_path
|
71
|
+
File.join(root, path_to_views)
|
72
|
+
end
|
73
|
+
|
74
|
+
def root
|
75
|
+
# Get the absolute path of the project who is using us
|
76
|
+
File.expand_path(Dir.pwd)
|
77
|
+
end
|
78
|
+
|
79
|
+
def gem_root
|
80
|
+
# Get the absolute path of our gem root
|
81
|
+
File.expand_path(File.dirname(__dir__))
|
82
|
+
end
|
83
|
+
|
84
|
+
def fetch(view_name)
|
85
|
+
view_name = symbolize_name(view_name)
|
86
|
+
list.fetch(view_name)
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
def configuration
|
92
|
+
@configuration ||= Configuration.new
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def symbolize_name(name)
|
97
|
+
name.is_a?(String) ? name.to_sym : name
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Monocle::BumpCommand
|
2
|
+
|
3
|
+
attr_reader :view
|
4
|
+
|
5
|
+
def initialize(view)
|
6
|
+
@view = view
|
7
|
+
end
|
8
|
+
|
9
|
+
def call
|
10
|
+
# Get the SQL from the file, skipping the timestamp row
|
11
|
+
# Drop the newlines too
|
12
|
+
sql = File.readlines(view.path_for_sql)[1..-1]
|
13
|
+
# Generate the new timestamp line
|
14
|
+
timestamp = ["-- Timestamp: #{Time.now}\n"]
|
15
|
+
# Put it back together
|
16
|
+
new_sql = (timestamp + sql).join
|
17
|
+
# Open the file for writing (no w+, we want to clear it up)
|
18
|
+
File.open(view.path_for_sql, "w") do |f|
|
19
|
+
f << new_sql
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Monocle
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :path_to_views, :logger
|
4
|
+
|
5
|
+
# Define a custom logger
|
6
|
+
def logger
|
7
|
+
@logger ||= (defined?(Rails) ? Rails.logger : Logger.new('monocle.log'))
|
8
|
+
end
|
9
|
+
|
10
|
+
# The relative path to where views are stored, relative to the root of the
|
11
|
+
# project
|
12
|
+
def path_to_views
|
13
|
+
@path_to_views ||= "db/views"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module Monocle::Generators
|
4
|
+
class InstallGenerator < Rails::Generators::Base
|
5
|
+
desc "Creates everything you need to start rolling with monocle"
|
6
|
+
def create_migration
|
7
|
+
invoke "migration", ["CreateMonocleMigrations", "version:string:uniq", "--primary-key-type=false"], options
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
|
3
|
+
module Monocle::Generators
|
4
|
+
class MatviewGenerator < Rails::Generators::NamedBase
|
5
|
+
desc "Creates a materialized view SQL template and optionally a model to go with it"
|
6
|
+
class_option :skip_model, type: :boolean, default: false, desc: "Skips model generation"
|
7
|
+
|
8
|
+
def generate_sql_file
|
9
|
+
create_file "db/views/#{file_name}.sql" do
|
10
|
+
<<-EOF
|
11
|
+
-- Timestamp: #{Time.now}
|
12
|
+
CREATE MATERIALIZED VIEW #{file_name} AS
|
13
|
+
-- Add your stuff here
|
14
|
+
;
|
15
|
+
EOF
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_model_file
|
20
|
+
# Don't do anything if we're skipping this
|
21
|
+
return if options[:skip_model]
|
22
|
+
# Invoke rails' nifty model generator for us
|
23
|
+
invoke "model", [file_path.singularize], options.merge(migration: false, test_framework: false)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
|
3
|
+
module Monocle::Generators
|
4
|
+
class ViewGenerator < Rails::Generators::NamedBase
|
5
|
+
desc "Creates a view SQL template and optionally a model to go with it"
|
6
|
+
class_option :skip_model, type: :boolean, default: false, desc: "Skips model generation"
|
7
|
+
|
8
|
+
def generate_sql_file
|
9
|
+
create_file "db/views/#{file_name}.sql" do
|
10
|
+
<<-EOF
|
11
|
+
-- Timestamp: #{Time.now}
|
12
|
+
CREATE OR REPLACE VIEW #{file_name} AS
|
13
|
+
-- Add your stuff here
|
14
|
+
;
|
15
|
+
EOF
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_model_file
|
20
|
+
# Don't do anything if we're skipping this
|
21
|
+
return if options[:skip_model]
|
22
|
+
# Invoke rails' nifty model generator for us
|
23
|
+
invoke "model", [file_path.singularize], options.merge(migration: false, test_framework: false)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Monocle
|
2
|
+
class ListCommand
|
3
|
+
delegate :views_path, to: Monocle
|
4
|
+
|
5
|
+
attr_reader :view_names
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@view_names = Dir[File.join(views_path, "*.sql")].map { |f| File.basename(f, ".sql") }
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
{}.tap do |hash|
|
13
|
+
view_names.each do |view_name|
|
14
|
+
hash[view_name.to_sym] = View.new(view_name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Monocle
|
2
|
+
class VersionGenerator
|
3
|
+
attr_reader :path, :view
|
4
|
+
|
5
|
+
def initialize(path)
|
6
|
+
@path = path
|
7
|
+
@view = File.basename(path, ".sql")
|
8
|
+
end
|
9
|
+
|
10
|
+
def generate
|
11
|
+
timestamp = File.open path, &:readline
|
12
|
+
fail "can't read timestamp of #{path}! Aborting..." unless timestamp.starts_with? "-- Timestamp: "
|
13
|
+
# Get only the digits out of the timestamp line
|
14
|
+
timestamp.gsub!(/[^\d]/, '')
|
15
|
+
"#{view}_#{timestamp}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/monocle/view.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# An in-memory representation of the view
|
2
|
+
module Monocle
|
3
|
+
class View
|
4
|
+
attr_reader :name
|
5
|
+
attr_accessor :dependants
|
6
|
+
|
7
|
+
delegate :views_path, :list, :versions, :logger, to: Monocle
|
8
|
+
delegate :info, :warn, :error, :debug, to: :logger
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@name = name
|
12
|
+
@dependants = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def materialized?
|
16
|
+
!!(@materialized ||= create_command =~ /MATERIALIZED VIEW/i)
|
17
|
+
end
|
18
|
+
|
19
|
+
def drop
|
20
|
+
debug "Dropping #{name}..."
|
21
|
+
self.dependants = get_dependants_from_pg
|
22
|
+
dependants.each &:drop
|
23
|
+
execute drop_command
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def create
|
28
|
+
debug "Creating #{name}..."
|
29
|
+
execute create_command
|
30
|
+
Migration.find_or_create_by version: slug
|
31
|
+
dependants.each &:create
|
32
|
+
true
|
33
|
+
rescue ActiveRecord::StatementInvalid => e
|
34
|
+
# We may have another new view coming that this view depend on
|
35
|
+
# if the relation name is included on our list of views, we create
|
36
|
+
# that first and then retry
|
37
|
+
if e.message =~ /PG::UndefinedTable/ &&
|
38
|
+
e.message.scan(/relation \"(\w+)\" does not exist/) &&
|
39
|
+
list.keys.include?($1.to_sym)
|
40
|
+
warn "Can't create #{name} because it depends on #{$1}, creating that first..."
|
41
|
+
list.fetch($1.to_sym).create
|
42
|
+
retry
|
43
|
+
else
|
44
|
+
fail e
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def migrate
|
49
|
+
if versions.include?(slug)
|
50
|
+
debug "Skipping #{name} as it's already up to date."
|
51
|
+
true
|
52
|
+
else
|
53
|
+
status = drop && create
|
54
|
+
info "#{name} migrated to #{slug}!"
|
55
|
+
status
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def refresh(concurrently: false)
|
60
|
+
# We don't refresh normal views
|
61
|
+
return false unless materialized?
|
62
|
+
_concurrently = " CONCURRENTLY" if concurrently
|
63
|
+
execute "REFRESH MATERIALIZED VIEW#{_concurrently} #{name}"
|
64
|
+
true
|
65
|
+
rescue ActiveRecord::StatementInvalid => e
|
66
|
+
# This view is trying to select from a different view that hasn't been
|
67
|
+
# populated.
|
68
|
+
if e.message =~ /PG::ObjectNotInPrerequisiteState/ &&
|
69
|
+
e.message.scan(/materialized view \"(\w+)\" has not been populated/) &&
|
70
|
+
list.keys.include?($1.to_sym)
|
71
|
+
warn "Can't refresh #{name} because it depends on #{$1} which hasn't been populated, refreshing that first..."
|
72
|
+
list.fetch($1.to_sym).refresh
|
73
|
+
retry
|
74
|
+
else
|
75
|
+
fail e
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def slug
|
80
|
+
@slug ||= VersionGenerator.new(path_for_sql).generate
|
81
|
+
end
|
82
|
+
|
83
|
+
def drop_command
|
84
|
+
_materialized = 'MATERIALIZED' if materialized?
|
85
|
+
"DROP #{_materialized} VIEW IF EXISTS #{name};"
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_command
|
89
|
+
@create_command ||= File.read(path_for_sql)
|
90
|
+
end
|
91
|
+
|
92
|
+
def path_for_sql
|
93
|
+
@path_for_sql ||= File.join views_path, "#{name}.sql"
|
94
|
+
end
|
95
|
+
|
96
|
+
def exists?
|
97
|
+
execute(check_if_view_exists_sql).entries.map(&:values).flatten.first
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
def check_if_view_exists_sql
|
103
|
+
<<-SQL
|
104
|
+
SELECT count(*) > 0
|
105
|
+
FROM pg_catalog.pg_class c
|
106
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
107
|
+
WHERE c.relkind in ('m','v')
|
108
|
+
AND n.nspname = 'public'
|
109
|
+
AND c.relname = '#{name}';
|
110
|
+
SQL
|
111
|
+
end
|
112
|
+
|
113
|
+
def get_dependants_from_pg
|
114
|
+
map_dependants(execute(find_dependants_sql).entries.map(&:values).flatten - [name])
|
115
|
+
end
|
116
|
+
|
117
|
+
def map_dependants(deps)
|
118
|
+
deps.map { |d| list[d.to_sym] }.compact
|
119
|
+
end
|
120
|
+
|
121
|
+
def find_dependants_sql
|
122
|
+
<<-SQL
|
123
|
+
WITH RECURSIVE vlist AS (
|
124
|
+
SELECT c.oid::REGCLASS AS view_name
|
125
|
+
FROM pg_class c
|
126
|
+
WHERE c.relname = '#{name}'
|
127
|
+
UNION ALL
|
128
|
+
SELECT DISTINCT r.ev_class::REGCLASS AS view_name
|
129
|
+
FROM pg_depend d
|
130
|
+
JOIN pg_rewrite r ON (r.oid = d.objid)
|
131
|
+
JOIN vlist ON (vlist.view_name = d.refobjid)
|
132
|
+
WHERE d.refobjsubid != 0
|
133
|
+
)
|
134
|
+
SELECT * FROM vlist;
|
135
|
+
SQL
|
136
|
+
end
|
137
|
+
|
138
|
+
def get_dependants_from_error(e)
|
139
|
+
map_dependants e.message.scan(/(\w+) depends on.+view #{name}/).flatten
|
140
|
+
end
|
141
|
+
|
142
|
+
def execute(sql)
|
143
|
+
ActiveRecord::Base.connection.execute(sql)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
namespace :monocle do
|
2
|
+
desc "List all Monocle managed views"
|
3
|
+
task :list => :environment do
|
4
|
+
Monocle.list
|
5
|
+
end
|
6
|
+
|
7
|
+
desc "List all Monocle view slugs"
|
8
|
+
task :versions => :environment do
|
9
|
+
Monocle.versions
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "Migrate any monocle views that need migratin'"
|
13
|
+
task :migrate => :environment do
|
14
|
+
Monocle.migrate
|
15
|
+
Rake::Task['db:structure:dump'].invoke
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Refreshes a given monocle view"
|
19
|
+
task :refresh, [:view_name] => :environment do |t, args|
|
20
|
+
Monocle.refresh(args.view_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Refreshes a given monocle view"
|
24
|
+
task :refresh_all => :environment do |t, args|
|
25
|
+
Monocle.refresh_all
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Bump a monocle view's timestamp by name"
|
29
|
+
task :bump, [:view_name] do |t, args|
|
30
|
+
Rake::Task['environment'].invoke
|
31
|
+
view_name = args.view_name
|
32
|
+
Monocle.bump(view_name)
|
33
|
+
end
|
34
|
+
end
|
data/monocle.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 'monocle/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "monocle-views"
|
8
|
+
spec.version = Monocle::VERSION
|
9
|
+
spec.authors = ["Leonardo Bighetti", "Eric Draut"]
|
10
|
+
spec.email = ["eric@invitedhome.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Monocle helps you manage your DB views.}
|
13
|
+
spec.description = %q{Monocle helps you manage your DB views.}
|
14
|
+
spec.homepage = "https://github.com/tommyfriendhusen08/monocle"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
26
|
+
spec.add_development_dependency "mocha"
|
27
|
+
spec.add_development_dependency "pry-nav"
|
28
|
+
spec.add_development_dependency "dotenv"
|
29
|
+
spec.add_development_dependency "pg"
|
30
|
+
spec.add_development_dependency "database_cleaner"
|
31
|
+
|
32
|
+
spec.add_dependency "rake"
|
33
|
+
spec.add_dependency "activesupport", ">= 4", "< 6"
|
34
|
+
spec.add_dependency "activerecord", ">= 4", "< 6"
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: monocle-views
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.7
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Leonardo Bighetti
|
8
|
+
- Eric Draut
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2018-06-08 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.14'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.14'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rspec
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '3.0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '3.0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: mocha
|
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: pry-nav
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: dotenv
|
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: pg
|
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: database_cleaner
|
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: rake
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :runtime
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: activesupport
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '4'
|
133
|
+
- - "<"
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '6'
|
136
|
+
type: :runtime
|
137
|
+
prerelease: false
|
138
|
+
version_requirements: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '4'
|
143
|
+
- - "<"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '6'
|
146
|
+
- !ruby/object:Gem::Dependency
|
147
|
+
name: activerecord
|
148
|
+
requirement: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '4'
|
153
|
+
- - "<"
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
version: '6'
|
156
|
+
type: :runtime
|
157
|
+
prerelease: false
|
158
|
+
version_requirements: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '4'
|
163
|
+
- - "<"
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '6'
|
166
|
+
description: Monocle helps you manage your DB views.
|
167
|
+
email:
|
168
|
+
- eric@invitedhome.com
|
169
|
+
executables: []
|
170
|
+
extensions: []
|
171
|
+
extra_rdoc_files: []
|
172
|
+
files:
|
173
|
+
- ".ruby-version"
|
174
|
+
- Gemfile
|
175
|
+
- Gemfile.lock
|
176
|
+
- LICENSE.txt
|
177
|
+
- README.md
|
178
|
+
- Rakefile
|
179
|
+
- bin/console
|
180
|
+
- bin/setup
|
181
|
+
- lib/monocle.rb
|
182
|
+
- lib/monocle/bump_command.rb
|
183
|
+
- lib/monocle/configuration.rb
|
184
|
+
- lib/monocle/generators/install_generator.rb
|
185
|
+
- lib/monocle/generators/matview_generator.rb
|
186
|
+
- lib/monocle/generators/view_generator.rb
|
187
|
+
- lib/monocle/list_command.rb
|
188
|
+
- lib/monocle/migration.rb
|
189
|
+
- lib/monocle/railtie.rb
|
190
|
+
- lib/monocle/version.rb
|
191
|
+
- lib/monocle/version_generator.rb
|
192
|
+
- lib/monocle/view.rb
|
193
|
+
- lib/tasks/monocle.rake
|
194
|
+
- monocle.gemspec
|
195
|
+
homepage: https://github.com/tommyfriendhusen08/monocle
|
196
|
+
licenses:
|
197
|
+
- MIT
|
198
|
+
metadata: {}
|
199
|
+
post_install_message:
|
200
|
+
rdoc_options: []
|
201
|
+
require_paths:
|
202
|
+
- lib
|
203
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
204
|
+
requirements:
|
205
|
+
- - ">="
|
206
|
+
- !ruby/object:Gem::Version
|
207
|
+
version: '0'
|
208
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
209
|
+
requirements:
|
210
|
+
- - ">="
|
211
|
+
- !ruby/object:Gem::Version
|
212
|
+
version: '0'
|
213
|
+
requirements: []
|
214
|
+
rubyforge_project:
|
215
|
+
rubygems_version: 2.6.11
|
216
|
+
signing_key:
|
217
|
+
specification_version: 4
|
218
|
+
summary: Monocle helps you manage your DB views.
|
219
|
+
test_files: []
|