viewy 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +9 -0
- data/db/migrate/20150929144540_add_view_replace_function.rb +75 -0
- data/db/migrate/20150929205301_add_view_dependencies.rb +80 -0
- data/lib/viewy/acts_as_materialized_view.rb +32 -0
- data/lib/viewy/acts_as_view.rb +22 -0
- data/lib/viewy/dependency_manager.rb +50 -0
- data/lib/viewy/engine.rb +8 -0
- data/lib/viewy/materialized_views.rb +7 -0
- data/lib/viewy/models/materialized_view_dependency.rb +11 -0
- data/lib/viewy/models.rb +7 -0
- data/lib/viewy/version.rb +1 -1
- data/lib/viewy.rb +5 -1
- metadata +10 -3
- data/LICENSE +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1796596ee3676d14308739bce83b2578bf45c96
|
4
|
+
data.tar.gz: 4f2a4a1c577ca5e08dd0f79aacdbdcb34c09c221
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c98415f23bbbf74f7235b97fcf906a0d2c9e9196f4e410a4be84052879ef651e287eab3743589159460c9f6591b06527bb9b4e99be444f5d5dca4230dd6d90b
|
7
|
+
data.tar.gz: 32b3c33a89c6e462d65a9b131f9bb8bccab58b82f21e55bdf118b08ea82defce73b4e6eaced63855bbc498eea4f1a3450363d4d00f793b44571a7ff83ef8c726
|
data/Rakefile
CHANGED
@@ -18,6 +18,15 @@ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
|
18
18
|
load 'rails/tasks/engine.rake'
|
19
19
|
|
20
20
|
|
21
|
+
load 'rails/tasks/statistics.rake'
|
22
|
+
|
23
|
+
require 'rspec/core'
|
24
|
+
require 'rspec/core/rake_task'
|
25
|
+
|
26
|
+
desc 'Run all specs in spec directory (excluding plugin specs)'
|
27
|
+
RSpec::Core::RakeTask.new(:spec)
|
28
|
+
|
29
|
+
task :default => :spec
|
21
30
|
|
22
31
|
Bundler::GemHelper.install_tasks
|
23
32
|
|
@@ -0,0 +1,75 @@
|
|
1
|
+
class AddViewReplaceFunction < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
function_sql = <<-SQL
|
4
|
+
CREATE OR REPLACE FUNCTION replace_view(view_name TEXT, new_sql TEXT) RETURNS VOID AS $$
|
5
|
+
DECLARE
|
6
|
+
ordered_oids INTEGER[] := ARRAY(
|
7
|
+
SELECT oid FROM
|
8
|
+
(
|
9
|
+
WITH RECURSIVE dependency_graph(oid, depth, path, cycle) AS (
|
10
|
+
SELECT oid, 1, ARRAY[oid], FALSE
|
11
|
+
FROM pg_class
|
12
|
+
WHERE relname = view_name
|
13
|
+
UNION
|
14
|
+
SELECT
|
15
|
+
rewrites.ev_class,
|
16
|
+
dg.depth + 1,
|
17
|
+
dg.path || rewrites.ev_class,
|
18
|
+
rewrites.ev_class = ANY(dg.path)
|
19
|
+
FROM dependency_graph dg
|
20
|
+
JOIN pg_depend dependents ON dependents.refobjid = dg.oid
|
21
|
+
JOIN pg_rewrite rewrites ON rewrites.oid = dependents.objid
|
22
|
+
WHERE NOT dg.cycle
|
23
|
+
)
|
24
|
+
SELECT dependency_graph.OID, MIN(depth) AS min_depth FROM dependency_graph GROUP BY dependency_graph.OID ORDER BY min_depth
|
25
|
+
) ordered_dependents
|
26
|
+
);
|
27
|
+
create_statements TEXT[] := '{}';
|
28
|
+
current_statement TEXT;
|
29
|
+
view_drop_statement TEXT;
|
30
|
+
object_id INT;
|
31
|
+
BEGIN
|
32
|
+
FOREACH object_id IN ARRAY ordered_oids LOOP
|
33
|
+
SELECT
|
34
|
+
(CASE
|
35
|
+
WHEN (SELECT TRUE FROM pg_views WHERE viewname = relname) THEN
|
36
|
+
'CREATE OR REPLACE VIEW ' || pg_class.relname || ' AS ' || pg_get_viewdef(oid)
|
37
|
+
WHEN (SELECT TRUE FROM pg_matviews WHERE matviewname = relname) THEN
|
38
|
+
'CREATE MATERIALIZED VIEW ' || pg_class.relname || ' AS ' || pg_get_viewdef(oid)
|
39
|
+
END)
|
40
|
+
INTO current_statement
|
41
|
+
FROM pg_class
|
42
|
+
WHERE oid = object_id
|
43
|
+
AND pg_class.relname != view_name;
|
44
|
+
IF current_statement IS NOT NULL THEN
|
45
|
+
create_statements = create_statements || current_statement;
|
46
|
+
END IF;
|
47
|
+
END LOOP;
|
48
|
+
SELECT
|
49
|
+
(
|
50
|
+
CASE
|
51
|
+
WHEN (SELECT TRUE FROM pg_views WHERE viewname = view_name) THEN
|
52
|
+
'DROP VIEW ' || view_name || ' CASCADE;'
|
53
|
+
WHEN (SELECT TRUE FROM pg_matviews WHERE matviewname = view_name) THEN
|
54
|
+
'DROP MATERIALIZED VIEW ' || view_name || ' CASCADE;'
|
55
|
+
END
|
56
|
+
)
|
57
|
+
INTO view_drop_statement;
|
58
|
+
EXECUTE view_drop_statement;
|
59
|
+
EXECUTE new_sql;
|
60
|
+
FOREACH current_statement IN ARRAY create_statements LOOP
|
61
|
+
EXECUTE current_statement;
|
62
|
+
END LOOP;
|
63
|
+
END;
|
64
|
+
$$ LANGUAGE plpgsql;
|
65
|
+
SQL
|
66
|
+
execute function_sql
|
67
|
+
end
|
68
|
+
|
69
|
+
def down
|
70
|
+
function_drop_sql = <<-SQL
|
71
|
+
DROP FUNCTION replace_view(view_name TEXT, new_sql TEXT);
|
72
|
+
SQL
|
73
|
+
execute function_drop_sql
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
class AddViewDependencies < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
view_dependency_function_sql = <<-SQL
|
4
|
+
CREATE OR REPLACE FUNCTION view_dependencies(materialized_view NAME)
|
5
|
+
RETURNS NAME[]
|
6
|
+
AS $$
|
7
|
+
WITH RECURSIVE dependency_graph(oid, depth, path, cycle) AS (
|
8
|
+
SELECT oid, 1, ARRAY[oid], FALSE
|
9
|
+
FROM pg_class
|
10
|
+
WHERE relname = materialized_view
|
11
|
+
UNION
|
12
|
+
SELECT
|
13
|
+
dependents.refobjid,
|
14
|
+
dg.depth + 1,
|
15
|
+
dg.path || dependents.refobjid,
|
16
|
+
dependents.refobjid = ANY(dg.path)
|
17
|
+
FROM dependency_graph dg
|
18
|
+
JOIN pg_rewrite rewrites ON rewrites.ev_class = dg.oid
|
19
|
+
JOIN pg_depend dependents ON dependents.objid = rewrites.oid
|
20
|
+
WHERE NOT dg.cycle
|
21
|
+
), dependencies AS(
|
22
|
+
SELECT
|
23
|
+
(SELECT relname FROM pg_class WHERE pg_class.OID = dependency_graph.oid) AS view_name,
|
24
|
+
dependency_graph.OID,
|
25
|
+
MIN(depth) AS min_depth
|
26
|
+
FROM dependency_graph
|
27
|
+
GROUP BY dependency_graph.OID ORDER BY min_depth
|
28
|
+
)
|
29
|
+
SELECT ARRAY(SELECT dependencies.view_name FROM
|
30
|
+
dependencies
|
31
|
+
JOIN pg_matviews ON pg_matviews.matviewname = dependencies.view_name
|
32
|
+
WHERE dependencies.view_name != materialized_view)
|
33
|
+
;
|
34
|
+
$$ LANGUAGE SQL;
|
35
|
+
SQL
|
36
|
+
execute view_dependency_function_sql
|
37
|
+
|
38
|
+
dependency_sql = <<-SQL
|
39
|
+
CREATE MATERIALIZED VIEW materialized_view_dependencies AS
|
40
|
+
WITH normal_view_dependencies AS (
|
41
|
+
SELECT viewname AS view_name, view_dependencies(viewname) FROM pg_views
|
42
|
+
)
|
43
|
+
SELECT matviewname AS view_name, view_dependencies(matviewname), TRUE AS materialized_view FROM pg_matviews WHERE matviewname != 'materialized_view_dependencies'
|
44
|
+
UNION
|
45
|
+
SELECT normal_view_dependencies.*, FALSE AS materialized_view FROM normal_view_dependencies WHERE array_length(normal_view_dependencies.view_dependencies, 1) > 0;
|
46
|
+
;
|
47
|
+
SQL
|
48
|
+
execute dependency_sql
|
49
|
+
|
50
|
+
trigger_sql = <<-SQL
|
51
|
+
CREATE OR REPLACE FUNCTION refresh_materialized_view_dependencies() RETURNS EVENT_TRIGGER AS $$
|
52
|
+
DECLARE
|
53
|
+
view_exists BOOLEAN := (SELECT TRUE FROM pg_class WHERE pg_class.relname = 'materialized_view_dependencies' AND pg_class.relkind = 'm');
|
54
|
+
BEGIN
|
55
|
+
RAISE NOTICE 'refreshing view dependency hierarchy';
|
56
|
+
IF view_exists THEN
|
57
|
+
REFRESH MATERIALIZED VIEW materialized_view_dependencies;
|
58
|
+
END IF;
|
59
|
+
END
|
60
|
+
$$ LANGUAGE plpgsql;
|
61
|
+
|
62
|
+
CREATE EVENT TRIGGER view_dependencies_update
|
63
|
+
ON DDL_COMMAND_END
|
64
|
+
WHEN TAG IN ('DROP VIEW', 'DROP MATERIALIZED VIEW', 'CREATE VIEW', 'CREATE MATERIALIZED VIEW', 'ALTER VIEW', 'ALTER MATERIALIZED VIEW')
|
65
|
+
EXECUTE PROCEDURE refresh_materialized_view_dependencies();
|
66
|
+
SQL
|
67
|
+
|
68
|
+
execute trigger_sql
|
69
|
+
end
|
70
|
+
|
71
|
+
def down
|
72
|
+
down_sql = <<-SQL
|
73
|
+
DROP MATERIALIZED VIEW materialized_view_dependencies;
|
74
|
+
DROP FUNCTION view_dependencies(materialized_view NAME);
|
75
|
+
DROP EVENT TRIGGER view_dependencies_update;
|
76
|
+
DROP FUNCTION refresh_materialized_view_dependencies();
|
77
|
+
SQL
|
78
|
+
execute down_sql
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Viewy
|
2
|
+
# Provides a wrapper for materialized views that allows them and their dependencies to be refreshed easily
|
3
|
+
module ActsAsMaterializedView
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
# Refreshes this view and all materialized views it depends on.
|
8
|
+
# NOTE: the look-up for dependencies can take a second to run.
|
9
|
+
#
|
10
|
+
# @raise [ActiveRecord::RecordNotFoundError] raised when the view #refresh! is called on does not exist
|
11
|
+
# @raise [ActiveRecord::StatementInvalidError] raised if a dependent view is somehow not refreshed correctly
|
12
|
+
#
|
13
|
+
# @return [PG::Result] the result of the refresh statement on the materialized view
|
14
|
+
def refresh!
|
15
|
+
view_dep = Viewy::Models::MaterializedViewDependency.find(table_name)
|
16
|
+
view_dep.view_dependencies.each do |view_dependency|
|
17
|
+
connection.execute("REFRESH MATERIALIZED VIEW #{view_dependency}")
|
18
|
+
end
|
19
|
+
refresh_without_dependencies!
|
20
|
+
end
|
21
|
+
|
22
|
+
# Refreshes this view without refreshing any dependencies
|
23
|
+
#
|
24
|
+
# @raise [ActiveRecord::StatementInvalidError] raised if a dependent view is somehow not refreshed correctly
|
25
|
+
#
|
26
|
+
# @return [PG::Result] the result of the refresh statement on the materialized view
|
27
|
+
def refresh_without_dependencies!
|
28
|
+
connection.execute("REFRESH MATERIALIZED VIEW #{table_name}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Viewy
|
2
|
+
# Provides a wrapper for SQL views that allows them and their materialized dependencies to be refreshed easily
|
3
|
+
module ActsAsView
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
# Refreshes this view and all materialized views it depends on.
|
8
|
+
# NOTE: the look-up for dependencies can take a second to run.
|
9
|
+
#
|
10
|
+
# @raise [ActiveRecord::RecordNotFoundError] raised when the view #refresh! is called on does not exist
|
11
|
+
# @raise [ActiveRecord::StatementInvalidError] raised if a dependent view is somehow not refreshed correctly
|
12
|
+
#
|
13
|
+
# @return [nil]
|
14
|
+
def refresh!
|
15
|
+
view_dep = Viewy::Models::MaterializedViewDependency.find(table_name)
|
16
|
+
view_dep.view_dependencies.each do |view_dependency|
|
17
|
+
connection.execute("REFRESH MATERIALIZED VIEW #{view_dependency}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
|
3
|
+
module Viewy
|
4
|
+
class DependencyManager
|
5
|
+
include TSort
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
connection.execute(refresh_sql('materialized_view_dependencies'))
|
9
|
+
end
|
10
|
+
|
11
|
+
def connection
|
12
|
+
ActiveRecord::Base.connection
|
13
|
+
end
|
14
|
+
|
15
|
+
def refresh_all_materialized_views
|
16
|
+
ordered_views.each do |view_name|
|
17
|
+
connection.execute(refresh_sql(view_name))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private def views
|
22
|
+
@views ||= generate_view_hash
|
23
|
+
end
|
24
|
+
|
25
|
+
private def generate_view_hash
|
26
|
+
views = Viewy::Models::MaterializedViewDependency.all.select(&:materialized_view).map do |dep|
|
27
|
+
[dep.view_name, dep.view_dependencies]
|
28
|
+
end
|
29
|
+
Hash[views]
|
30
|
+
end
|
31
|
+
|
32
|
+
private def tsort_each_node
|
33
|
+
views.each_key do |view|
|
34
|
+
yield view
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private def tsort_each_child(view, &block)
|
39
|
+
views[view].each(&block)
|
40
|
+
end
|
41
|
+
|
42
|
+
private def refresh_sql(name)
|
43
|
+
<<-SQL.strip
|
44
|
+
REFRESH MATERIALIZED VIEW #{name}
|
45
|
+
SQL
|
46
|
+
end
|
47
|
+
|
48
|
+
alias_method :ordered_views, :tsort
|
49
|
+
end
|
50
|
+
end
|
data/lib/viewy/engine.rb
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
module Viewy
|
2
2
|
class Engine < ::Rails::Engine
|
3
3
|
isolate_namespace Viewy
|
4
|
+
|
5
|
+
initializer :append_migrations do |app|
|
6
|
+
unless app.root.to_s.match root.to_s
|
7
|
+
config.paths["db/migrate"].expanded.each do |path|
|
8
|
+
app.config.paths["db/migrate"] << path
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
4
12
|
end
|
5
13
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Viewy
|
2
|
+
module Models
|
3
|
+
# Provides a means of accessing information about view dependencies from within a Rails app.
|
4
|
+
# The foreign key of the dependency information table is the name of the view that a user needs
|
5
|
+
# dependency information about.
|
6
|
+
class MaterializedViewDependency < ActiveRecord::Base
|
7
|
+
self.table_name = 'materialized_view_dependencies'
|
8
|
+
self.primary_key = 'view_name'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/lib/viewy/models.rb
ADDED
data/lib/viewy/version.rb
CHANGED
data/lib/viewy.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: viewy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Emerson Huitt
|
@@ -74,15 +74,22 @@ executables: []
|
|
74
74
|
extensions: []
|
75
75
|
extra_rdoc_files: []
|
76
76
|
files:
|
77
|
-
- LICENSE
|
78
77
|
- README.rdoc
|
79
78
|
- Rakefile
|
80
79
|
- config/routes.rb
|
80
|
+
- db/migrate/20150929144540_add_view_replace_function.rb
|
81
|
+
- db/migrate/20150929205301_add_view_dependencies.rb
|
81
82
|
- lib/tasks/viewy_tasks.rake
|
82
83
|
- lib/viewy.rb
|
84
|
+
- lib/viewy/acts_as_materialized_view.rb
|
85
|
+
- lib/viewy/acts_as_view.rb
|
86
|
+
- lib/viewy/dependency_manager.rb
|
83
87
|
- lib/viewy/engine.rb
|
88
|
+
- lib/viewy/materialized_views.rb
|
89
|
+
- lib/viewy/models.rb
|
90
|
+
- lib/viewy/models/materialized_view_dependency.rb
|
84
91
|
- lib/viewy/version.rb
|
85
|
-
homepage:
|
92
|
+
homepage: http://www.scimedsolutions.com
|
86
93
|
licenses:
|
87
94
|
- MIT
|
88
95
|
metadata: {}
|
data/LICENSE
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
Copyright 2015 YOURNAME
|
2
|
-
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
-
a copy of this software and associated documentation files (the
|
5
|
-
"Software"), to deal in the Software without restriction, including
|
6
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
-
permit persons to whom the Software is furnished to do so, subject to
|
9
|
-
the following conditions:
|
10
|
-
|
11
|
-
The above copyright notice and this permission notice shall be
|
12
|
-
included in all copies or substantial portions of the Software.
|
13
|
-
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|