spectacles 1.2.0 → 2.0.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.
- checksums.yaml +5 -5
- data/.travis.yml +11 -7
- data/Gemfile +0 -1
- data/LICENSE +1 -1
- data/Rakefile +1 -1
- data/Readme.rdoc +5 -3
- data/lib/spectacles/materialized_view.rb +11 -3
- data/lib/spectacles/schema_statements/abstract_adapter.rb +8 -4
- data/lib/spectacles/schema_statements/mysql2_adapter.rb +41 -3
- data/lib/spectacles/schema_statements/postgresql_adapter.rb +5 -1
- data/lib/spectacles/schema_statements/sqlite3_adapter.rb +1 -1
- data/lib/spectacles/version.rb +1 -1
- data/lib/spectacles/view.rb +1 -2
- data/specs/adapters/mysql2_adapter_spec.rb +1 -1
- data/specs/adapters/postgresql_adapter_spec.rb +7 -7
- data/specs/spec_helper.rb +1 -1
- data/specs/spectacles/abstract_adapter_override_spec.rb +1 -1
- data/specs/spectacles/schema_statements/abstract_adapter_spec.rb +15 -14
- data/specs/spectacles/view_spec.rb +2 -2
- data/specs/support/schema_statement_examples.rb +68 -50
- data/specs/support/view_examples.rb +10 -8
- data/spectacles.gemspec +4 -4
- metadata +17 -8
- data/lib/spectacles/schema_statements/mysql_adapter.rb +0 -39
- data/specs/adapters/mysql_adapter_spec.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1c759a317da5654cfbfb8aadbabe40a10122b75fa2cd0a569b9d7cb6792b490a
|
4
|
+
data.tar.gz: a76b8d1e07255df03cfed644e868cc7bd4f153ce189e8e457fd2e155f09b5398
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 25f113ee6cfc34e1989378d3710c899a340271a37879be95aea22817fbb86ad09c14fb978c90b20a0dd5476f895739fdc902d97b6b5a288a9ae012291736b2dd
|
7
|
+
data.tar.gz: d372e0d4dae6f83b2396f582012de107a39cdfd0d57acfb77974aa9d5b9297972eb111b6eb07862c0d34241075392daddc6ef3fdd47224b8b2b9f85035a5723a
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/LICENSE
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2012 Adam Hutchison, Brandon Dewitt
|
3
|
+
Copyright (c) 2012-2019 Adam Hutchison, Brandon Dewitt
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
6
|
this software and associated documentation files (the "Software"), to deal in
|
data/Rakefile
CHANGED
data/Readme.rdoc
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
{<img src="https://travis-ci.org/liveh2o/spectacles.svg?branch=master" alt="Build Status" />}[https://travis-ci.org/liveh2o/spectacles] {<img src="https://badge.fury.io/rb/spectacles.svg" alt="Gem Version" />}[https://badge.fury.io/rb/spectacles]
|
2
|
+
|
1
3
|
= Spectacles
|
2
4
|
|
3
|
-
Spectacles adds database view functionality to ActiveRecord. It is heavily inspired by Rails SQL Views (
|
5
|
+
Spectacles adds database view functionality to ActiveRecord. It is heavily inspired by Rails SQL Views (created by https://github.com/aeden but no longer maintained) and built from the ground up to work with Rails 3.2+.
|
4
6
|
|
5
7
|
Spectacles provides the ability to create views in migrations using a similar format to creating tables. It also provides an abstract view class that inherits from ActiveRecord::Base that can be used to create view-backed models.
|
6
8
|
|
@@ -57,7 +59,7 @@ they are kind of a cross between tables (which persist data) and views
|
|
57
59
|
# just like Spectacles::View
|
58
60
|
end
|
59
61
|
|
60
|
-
Because
|
62
|
+
Because materialized views cache a snapshot of the data as it
|
61
63
|
exists at a point in time (typically when the view was created), you
|
62
64
|
need to manually _refresh_ the view when new data is added to the
|
63
65
|
original tables. You can do this with the +#refresh!+ method on
|
@@ -115,4 +117,4 @@ to affect how the new view is created:
|
|
115
117
|
|
116
118
|
= License
|
117
119
|
|
118
|
-
Spectacles is licensed under MIT license (Read
|
120
|
+
Spectacles is licensed under MIT license (Read the LICENSE file for full license)
|
@@ -3,15 +3,23 @@ module Spectacles
|
|
3
3
|
self.abstract_class = true
|
4
4
|
|
5
5
|
def self.new(*)
|
6
|
-
raise NotImplementedError
|
6
|
+
raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
|
7
7
|
end
|
8
8
|
|
9
9
|
def self.materialized_view_exists?
|
10
10
|
self.connection.materialized_view_exists?(self.view_name)
|
11
11
|
end
|
12
12
|
|
13
|
-
def self.refresh!
|
14
|
-
|
13
|
+
def self.refresh!(concurrently: false)
|
14
|
+
if concurrently
|
15
|
+
self.connection.refresh_materialized_view_concurrently(self.view_name)
|
16
|
+
else
|
17
|
+
self.connection.refresh_materialized_view(self.view_name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.refresh_concurrently!
|
22
|
+
refresh!(concurrently: true)
|
15
23
|
end
|
16
24
|
|
17
25
|
class << self
|
@@ -19,8 +19,8 @@ module Spectacles
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def create_view_statement(view_name, create_query)
|
22
|
-
query = "CREATE VIEW ? AS #{create_query}"
|
23
|
-
query_array = [query, view_name.to_s]
|
22
|
+
#query = "CREATE VIEW ? AS #{create_query}"
|
23
|
+
#query_array = [query, view_name.to_s]
|
24
24
|
|
25
25
|
#return ActiveRecord::Base.__send__(:sanitize_sql_array, query_array)
|
26
26
|
"CREATE VIEW #{view_name} AS #{create_query}"
|
@@ -32,8 +32,8 @@ module Spectacles
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def drop_view_statement(view_name)
|
35
|
-
query = "DROP VIEW IF EXISTS ? "
|
36
|
-
query_array = [query, view_name.to_s]
|
35
|
+
#query = "DROP VIEW IF EXISTS ? "
|
36
|
+
#query_array = [query, view_name.to_s]
|
37
37
|
|
38
38
|
#return ActiveRecord::Base.__send__(:sanitize_sql_array, query_array)
|
39
39
|
"DROP VIEW IF EXISTS #{view_name} "
|
@@ -74,6 +74,10 @@ module Spectacles
|
|
74
74
|
def refresh_materialized_view(view_name)
|
75
75
|
raise NotImplementedError, "Override refresh_materialized_view for your db adapter in #{self.class}"
|
76
76
|
end
|
77
|
+
|
78
|
+
def refresh_materialized_view_concurrently(view_name)
|
79
|
+
raise NotImplementedError, "Override refresh_materialized_view_concurrently for your db adapter in #{self.class}"
|
80
|
+
end
|
77
81
|
end
|
78
82
|
end
|
79
83
|
end
|
@@ -1,9 +1,47 @@
|
|
1
|
-
require 'spectacles/schema_statements/mysql_adapter'
|
2
|
-
|
3
1
|
module Spectacles
|
4
2
|
module SchemaStatements
|
5
3
|
module Mysql2Adapter
|
6
|
-
include Spectacles::SchemaStatements::
|
4
|
+
include Spectacles::SchemaStatements::AbstractAdapter
|
5
|
+
|
6
|
+
# overrides the #tables method from ActiveRecord's MysqlAdapter
|
7
|
+
# to return only tables, and not views.
|
8
|
+
def tables(name = nil, database = nil, like = nil)
|
9
|
+
database = database ? quote_table_name(database) : "DATABASE()"
|
10
|
+
by_name = like ? "AND table_name LIKE #{quote(like)}" : ""
|
11
|
+
|
12
|
+
sql = <<-SQL.squish
|
13
|
+
SELECT table_name, table_type
|
14
|
+
FROM information_schema.tables
|
15
|
+
WHERE table_schema = #{database}
|
16
|
+
AND table_type = 'BASE TABLE'
|
17
|
+
#{by_name}
|
18
|
+
SQL
|
19
|
+
|
20
|
+
execute_and_free(sql, 'SCHEMA') do |result|
|
21
|
+
rows_from(result).map(&:first)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def views(name = nil) #:nodoc:
|
26
|
+
result = execute("SHOW FULL TABLES WHERE TABLE_TYPE='VIEW'")
|
27
|
+
|
28
|
+
rows_from(result).map(&:first)
|
29
|
+
end
|
30
|
+
|
31
|
+
def view_build_query(view, name = nil)
|
32
|
+
result = execute("SHOW CREATE VIEW #{view}", name)
|
33
|
+
algorithm_string = rows_from(result).first[1]
|
34
|
+
|
35
|
+
algorithm_string.gsub(/CREATE .*? (AS)+/i, "")
|
36
|
+
rescue ActiveRecord::StatementInvalid => e
|
37
|
+
raise "No view called #{view} found, #{e}"
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def rows_from(result)
|
43
|
+
result.respond_to?(:rows) ? result.rows : result
|
44
|
+
end
|
7
45
|
end
|
8
46
|
end
|
9
47
|
end
|
@@ -68,7 +68,7 @@ module Spectacles
|
|
68
68
|
definition = row["definition"].strip.sub(/;$/, "")
|
69
69
|
|
70
70
|
options = {}
|
71
|
-
options[:data] = false if ispopulated == 'f'
|
71
|
+
options[:data] = false if ispopulated == 'f' || ispopulated == false
|
72
72
|
options[:storage] = parse_storage_definition(storage) if storage.present?
|
73
73
|
options[:tablespace] = tablespace if tablespace.present?
|
74
74
|
|
@@ -135,6 +135,10 @@ module Spectacles
|
|
135
135
|
execute "REFRESH MATERIALIZED VIEW #{quote_table_name(view_name)}"
|
136
136
|
end
|
137
137
|
|
138
|
+
def refresh_materialized_view_concurrently(view_name)
|
139
|
+
execute "REFRESH MATERIALIZED VIEW CONCURRENTLY #{quote_table_name(view_name)}"
|
140
|
+
end
|
141
|
+
|
138
142
|
def parse_storage_definition(storage)
|
139
143
|
# JRuby 9000 returns storage as an Array, whereas
|
140
144
|
# MRI returns a string.
|
data/lib/spectacles/version.rb
CHANGED
data/lib/spectacles/view.rb
CHANGED
@@ -3,8 +3,7 @@ module Spectacles
|
|
3
3
|
self.abstract_class = true
|
4
4
|
|
5
5
|
def self.new(*)
|
6
|
-
|
7
|
-
super # raise NotImplementedError, "#{self} is an abstract class and can not be instantiated."
|
6
|
+
raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
|
8
7
|
end
|
9
8
|
|
10
9
|
def self.view_exists?
|
@@ -25,44 +25,44 @@ describe "Spectacles::SchemaStatements::PostgreSQLAdapter" do
|
|
25
25
|
|
26
26
|
describe "#view_build_query" do
|
27
27
|
it "should escape double-quotes returned by Postgres" do
|
28
|
-
test_base.view_build_query(:new_product_users).must_match(/\\"/)
|
28
|
+
_(test_base.view_build_query(:new_product_users)).must_match(/\\"/)
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
describe "#materialized_views" do
|
33
33
|
it "should support materialized views" do
|
34
|
-
test_base.supports_materialized_views
|
34
|
+
_(test_base.supports_materialized_views?).must_equal true
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
38
|
describe "#create_materialized_view_statement" do
|
39
39
|
it "should work with no options" do
|
40
40
|
query = test_base.create_materialized_view_statement(:view_name, "select_query_here")
|
41
|
-
query.must_match(/create materialized view view_name as select_query_here with data/i)
|
41
|
+
_(query).must_match(/create materialized view view_name as select_query_here with data/i)
|
42
42
|
end
|
43
43
|
|
44
44
|
it "should allow column names to be specified" do
|
45
45
|
query = test_base.create_materialized_view_statement(:view_name, "select_query_here",
|
46
46
|
columns: %i(first second third))
|
47
|
-
query.must_match(/create materialized view view_name \(first,second,third\) as select_query_here with data/i)
|
47
|
+
_(query).must_match(/create materialized view view_name \(first,second,third\) as select_query_here with data/i)
|
48
48
|
end
|
49
49
|
|
50
50
|
it "should allow storage parameters to be specified" do
|
51
51
|
query = test_base.create_materialized_view_statement(:view_name, "select_query_here",
|
52
52
|
storage: { bats_in_belfry: true, max_wingspan: 15 })
|
53
|
-
query.must_match(/create materialized view view_name with \(bats_in_belfry=true, max_wingspan=15\) as select_query_here with data/i)
|
53
|
+
_(query).must_match(/create materialized view view_name with \(bats_in_belfry=true, max_wingspan=15\) as select_query_here with data/i)
|
54
54
|
end
|
55
55
|
|
56
56
|
it "should allow tablespace to be specified" do
|
57
57
|
query = test_base.create_materialized_view_statement(:view_name, "select_query_here",
|
58
58
|
tablespace: :the_final_frontier)
|
59
|
-
query.must_match(/create materialized view view_name tablespace the_final_frontier as select_query_here with data/i)
|
59
|
+
_(query).must_match(/create materialized view view_name tablespace the_final_frontier as select_query_here with data/i)
|
60
60
|
end
|
61
61
|
|
62
62
|
it "should allow empty view to be created" do
|
63
63
|
query = test_base.create_materialized_view_statement(:view_name, "select_query_here",
|
64
64
|
data: false)
|
65
|
-
query.must_match(/create materialized view view_name as select_query_here with no data/i)
|
65
|
+
_(query).must_match(/create materialized view view_name as select_query_here with no data/i)
|
66
66
|
end
|
67
67
|
end
|
68
68
|
end
|
data/specs/spec_helper.rb
CHANGED
@@ -9,6 +9,6 @@ describe "loading an adapter" do
|
|
9
9
|
end
|
10
10
|
load File.join(__dir__, '../../lib/spectacles/abstract_adapter_override.rb')
|
11
11
|
Class.new(ActiveRecord::ConnectionAdapters::AbstractAdapter)
|
12
|
-
ActiveRecord::ConnectionAdapters::AbstractAdapter.instance_variable_get("@_spectacles_inherited_called").must_equal true
|
12
|
+
_(ActiveRecord::ConnectionAdapters::AbstractAdapter.instance_variable_get("@_spectacles_inherited_called")).must_equal true
|
13
13
|
end
|
14
14
|
end
|
@@ -1,10 +1,11 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Spectacles::SchemaStatements::AbstractAdapter do
|
3
|
+
describe Spectacles::SchemaStatements::AbstractAdapter do
|
4
4
|
class TestBase
|
5
5
|
extend Spectacles::SchemaStatements::AbstractAdapter
|
6
6
|
|
7
7
|
def self.materialized_views
|
8
|
+
@materialized_views ||= nil
|
8
9
|
@materialized_views || super
|
9
10
|
end
|
10
11
|
|
@@ -17,64 +18,64 @@ describe Spectacles::SchemaStatements::AbstractAdapter do
|
|
17
18
|
end
|
18
19
|
|
19
20
|
describe "#create_view" do
|
20
|
-
it "throws error when block not given and no build_query" do
|
21
|
-
lambda { TestBase.create_view(:view_name) }.must_raise(RuntimeError)
|
21
|
+
it "throws error when block not given and no build_query" do
|
22
|
+
_(lambda { TestBase.create_view(:view_name) }).must_raise(RuntimeError)
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
25
|
-
describe "#views" do
|
26
|
-
it "throws error when accessed on AbstractAdapter" do
|
27
|
-
lambda { TestBase.views }.must_raise(RuntimeError)
|
26
|
+
describe "#views" do
|
27
|
+
it "throws error when accessed on AbstractAdapter" do
|
28
|
+
_(lambda { TestBase.views }).must_raise(RuntimeError)
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
31
32
|
describe "#supports_materialized_views?" do
|
32
33
|
it "returns false when accessed on AbstractAdapter" do
|
33
|
-
TestBase.supports_materialized_views
|
34
|
+
_(TestBase.supports_materialized_views?).must_equal false
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
37
38
|
describe "#materialized_views" do
|
38
39
|
it "throws error when accessed on AbstractAdapter" do
|
39
|
-
lambda { TestBase.materialized_views }.must_raise(NotImplementedError)
|
40
|
+
_(lambda { TestBase.materialized_views }).must_raise(NotImplementedError)
|
40
41
|
end
|
41
42
|
end
|
42
43
|
|
43
44
|
describe "#materialized_view_exists?" do
|
44
45
|
it "is true when materialized_views includes the view" do
|
45
46
|
TestBase.with_materialized_views(%w(alpha beta gamma)) do
|
46
|
-
TestBase.materialized_view_exists?(:beta).must_equal true
|
47
|
+
_(TestBase.materialized_view_exists?(:beta)).must_equal true
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
50
51
|
it "is false when materialized_views does not include the view" do
|
51
52
|
TestBase.with_materialized_views(%w(alpha beta gamma)) do
|
52
|
-
TestBase.materialized_view_exists?(:delta).must_equal false
|
53
|
+
_(TestBase.materialized_view_exists?(:delta)).must_equal false
|
53
54
|
end
|
54
55
|
end
|
55
56
|
end
|
56
57
|
|
57
58
|
describe "#materialized_view_build_query" do
|
58
59
|
it "throws error when accessed on AbstractAdapter" do
|
59
|
-
lambda { TestBase.materialized_view_build_query(:books) }.must_raise(NotImplementedError)
|
60
|
+
_(lambda { TestBase.materialized_view_build_query(:books) }).must_raise(NotImplementedError)
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
63
64
|
describe "#create_materialized_view" do
|
64
65
|
it "throws error when accessed on AbstractAdapter" do
|
65
|
-
lambda { TestBase.create_materialized_view(:books) }.must_raise(NotImplementedError)
|
66
|
+
_(lambda { TestBase.create_materialized_view(:books) }).must_raise(NotImplementedError)
|
66
67
|
end
|
67
68
|
end
|
68
69
|
|
69
70
|
describe "#drop_materialized_view" do
|
70
71
|
it "throws error when accessed on AbstractAdapter" do
|
71
|
-
lambda { TestBase.drop_materialized_view(:books) }.must_raise(NotImplementedError)
|
72
|
+
_(lambda { TestBase.drop_materialized_view(:books) }).must_raise(NotImplementedError)
|
72
73
|
end
|
73
74
|
end
|
74
75
|
|
75
76
|
describe "#refresh_materialized_view" do
|
76
77
|
it "throws error when accessed on AbstractAdapter" do
|
77
|
-
lambda { TestBase.refresh_materialized_view(:books) }.must_raise(NotImplementedError)
|
78
|
+
_(lambda { TestBase.refresh_materialized_view(:books) }).must_raise(NotImplementedError)
|
78
79
|
end
|
79
80
|
end
|
80
81
|
|
@@ -1,65 +1,66 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
1
|
shared_examples_for "an adapter" do |adapter|
|
4
2
|
shared_base = Class.new do
|
5
3
|
extend Spectacles::SchemaStatements.const_get(adapter)
|
6
4
|
def self.quote_table_name(name); name; end
|
7
5
|
def self.quote_column_name(name); name; end
|
8
|
-
def self.execute(query); query; end
|
6
|
+
def self.execute(query); query; end
|
9
7
|
end
|
10
8
|
|
11
|
-
describe "ActiveRecord::SchemaDumper#dump" do
|
9
|
+
describe "ActiveRecord::SchemaDumper#dump" do
|
12
10
|
before(:each) do
|
13
11
|
ActiveRecord::Base.connection.drop_view(:new_product_users)
|
14
12
|
|
15
|
-
ActiveRecord::Base.connection.create_view(:new_product_users) do
|
13
|
+
ActiveRecord::Base.connection.create_view(:new_product_users) do
|
16
14
|
"SELECT name AS product_name, first_name AS username FROM
|
17
15
|
products JOIN users ON users.id = products.user_id"
|
18
16
|
end
|
19
17
|
|
20
18
|
if ActiveRecord::Base.connection.supports_materialized_views?
|
21
|
-
ActiveRecord::Base.connection.
|
19
|
+
ActiveRecord::Base.connection.drop_materialized_view(:materialized_product_users)
|
20
|
+
ActiveRecord::Base.connection.drop_materialized_view(:empty_materialized_product_users)
|
21
|
+
|
22
|
+
ActiveRecord::Base.connection.create_materialized_view(:materialized_product_users, force: true) do
|
22
23
|
"SELECT name AS product_name, first_name AS username FROM
|
23
24
|
products JOIN users ON users.id = products.user_id"
|
24
25
|
end
|
25
26
|
|
26
27
|
ActiveRecord::Base.connection.add_index :materialized_product_users, :product_name
|
27
28
|
|
28
|
-
ActiveRecord::Base.connection.create_materialized_view(:empty_materialized_product_users, storage: { fillfactor: 50 }, data: false, force: true) do
|
29
|
+
ActiveRecord::Base.connection.create_materialized_view(:empty_materialized_product_users, storage: { fillfactor: 50 }, data: false, force: true) do
|
29
30
|
"SELECT name AS product_name, first_name AS username FROM
|
30
31
|
products JOIN users ON users.id = products.user_id"
|
31
32
|
end
|
32
33
|
end
|
33
34
|
end
|
34
35
|
|
35
|
-
it "should return create_view in dump stream" do
|
36
|
+
it "should return create_view in dump stream" do
|
36
37
|
stream = StringIO.new
|
37
38
|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
|
38
|
-
stream.string.must_match(/create_view/)
|
39
|
+
_(stream.string).must_match(/create_view/)
|
39
40
|
end
|
40
41
|
|
41
42
|
if ActiveRecord::Base.connection.supports_materialized_views?
|
42
|
-
it "should return create_materialized_view in dump stream" do
|
43
|
+
it "should return create_materialized_view in dump stream" do
|
43
44
|
stream = StringIO.new
|
44
45
|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
|
45
|
-
stream.string.must_match(/create_materialized_view/)
|
46
|
+
_(stream.string).must_match(/create_materialized_view/)
|
46
47
|
end
|
47
48
|
|
48
|
-
it
|
49
|
+
it "should return add_index in dump stream" do
|
49
50
|
stream = StringIO.new
|
50
51
|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
|
51
|
-
stream.string.must_match(/add_index/)
|
52
|
+
_(stream.string).must_match(/add_index/)
|
52
53
|
end
|
53
54
|
|
54
55
|
it "should include options for create_materialized_view" do
|
55
56
|
stream = StringIO.new
|
56
57
|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
|
57
|
-
stream.string.must_match(/create_materialized_view.*fillfactor: 50/)
|
58
|
-
stream.string.must_match(/create_materialized_view.*data: false/)
|
58
|
+
_(stream.string).must_match(/create_materialized_view.*fillfactor: 50/)
|
59
|
+
_(stream.string).must_match(/create_materialized_view.*data: false/)
|
59
60
|
end
|
60
61
|
end
|
61
62
|
|
62
|
-
it "should rebuild views in dump stream" do
|
63
|
+
it "should rebuild views in dump stream" do
|
63
64
|
stream = StringIO.new
|
64
65
|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
|
65
66
|
|
@@ -79,10 +80,10 @@ shared_examples_for "an adapter" do |adapter|
|
|
79
80
|
|
80
81
|
eval(stream.string)
|
81
82
|
|
82
|
-
ActiveRecord::Base.connection.views.must_include('new_product_users')
|
83
|
+
_(ActiveRecord::Base.connection.views).must_include('new_product_users')
|
83
84
|
|
84
85
|
if ActiveRecord::Base.connection.supports_materialized_views?
|
85
|
-
ActiveRecord::Base.connection.materialized_views.must_include('materialized_product_users')
|
86
|
+
_(ActiveRecord::Base.connection.materialized_views).must_include('materialized_product_users')
|
86
87
|
end
|
87
88
|
end
|
88
89
|
end
|
@@ -90,41 +91,41 @@ shared_examples_for "an adapter" do |adapter|
|
|
90
91
|
describe "#create_view" do
|
91
92
|
let(:view_name) { :view_name }
|
92
93
|
|
93
|
-
it "throws error when block not given and no build_query" do
|
94
|
-
lambda { shared_base.create_view(view_name) }.must_raise(RuntimeError)
|
94
|
+
it "throws error when block not given and no build_query" do
|
95
|
+
_(lambda { shared_base.create_view(view_name) }).must_raise(RuntimeError)
|
95
96
|
end
|
96
97
|
|
97
98
|
describe "view_name" do
|
98
|
-
it "takes a symbol as the view_name" do
|
99
|
-
shared_base.create_view(view_name.to_sym, Product.all).must_match(/#{view_name}/)
|
99
|
+
it "takes a symbol as the view_name" do
|
100
|
+
_(shared_base.create_view(view_name.to_sym, Product.all)).must_match(/#{view_name}/)
|
100
101
|
end
|
101
102
|
|
102
|
-
it "takes a string as the view_name" do
|
103
|
-
shared_base.create_view(view_name.to_s, Product.all).must_match(/#{view_name}/)
|
103
|
+
it "takes a string as the view_name" do
|
104
|
+
_(shared_base.create_view(view_name.to_s, Product.all)).must_match(/#{view_name}/)
|
104
105
|
end
|
105
106
|
end
|
106
107
|
|
107
|
-
describe "build_query" do
|
108
|
-
it "uses a string if passed" do
|
108
|
+
describe "build_query" do
|
109
|
+
it "uses a string if passed" do
|
109
110
|
select_statement = "SELECT * FROM products"
|
110
|
-
shared_base.create_view(view_name, select_statement).must_match(/#{Regexp.escape(select_statement)}/)
|
111
|
+
_(shared_base.create_view(view_name, select_statement)).must_match(/#{Regexp.escape(select_statement)}/)
|
111
112
|
end
|
112
113
|
|
113
|
-
it "uses an Arel::Relation if passed" do
|
114
|
+
it "uses an Arel::Relation if passed" do
|
114
115
|
select_statement = Product.all.to_sql
|
115
|
-
shared_base.create_view(view_name, Product.all).must_match(/#{Regexp.escape(select_statement)}/)
|
116
|
+
_(shared_base.create_view(view_name, Product.all)).must_match(/#{Regexp.escape(select_statement)}/)
|
116
117
|
end
|
117
118
|
end
|
118
119
|
|
119
|
-
describe "block" do
|
120
|
-
it "can use an Arel::Relation from the yield" do
|
120
|
+
describe "block" do
|
121
|
+
it "can use an Arel::Relation from the yield" do
|
121
122
|
select_statement = Product.all.to_sql
|
122
|
-
shared_base.create_view(view_name) { Product.all }.must_match(/#{Regexp.escape(select_statement)}/)
|
123
|
+
_(shared_base.create_view(view_name) { Product.all }).must_match(/#{Regexp.escape(select_statement)}/)
|
123
124
|
end
|
124
125
|
|
125
|
-
it "can use a String from the yield" do
|
126
|
+
it "can use a String from the yield" do
|
126
127
|
select_statement = "SELECT * FROM products"
|
127
|
-
shared_base.create_view(view_name) { "SELECT * FROM products" }.must_match(/#{Regexp.escape(select_statement)}/)
|
128
|
+
_(shared_base.create_view(view_name) { "SELECT * FROM products" }).must_match(/#{Regexp.escape(select_statement)}/)
|
128
129
|
end
|
129
130
|
end
|
130
131
|
end
|
@@ -133,55 +134,72 @@ shared_examples_for "an adapter" do |adapter|
|
|
133
134
|
let(:view_name) { :view_name }
|
134
135
|
|
135
136
|
describe "view_name" do
|
136
|
-
it "takes a symbol as the view_name" do
|
137
|
-
shared_base.drop_view(view_name.to_sym).must_match(/#{view_name}/)
|
137
|
+
it "takes a symbol as the view_name" do
|
138
|
+
_(shared_base.drop_view(view_name.to_sym)).must_match(/#{view_name}/)
|
138
139
|
end
|
139
140
|
|
140
|
-
it "takes a string as the view_name" do
|
141
|
-
shared_base.drop_view(view_name.to_s).must_match(/#{view_name}/)
|
141
|
+
it "takes a string as the view_name" do
|
142
|
+
_(shared_base.drop_view(view_name.to_s)).must_match(/#{view_name}/)
|
142
143
|
end
|
143
144
|
end
|
144
145
|
end
|
145
146
|
|
147
|
+
describe "#tables" do
|
148
|
+
it "returns an array of all table names" do
|
149
|
+
_(ActiveRecord::Base.connection.tables).must_include("products")
|
150
|
+
_(ActiveRecord::Base.connection.tables).must_include("users")
|
151
|
+
end
|
152
|
+
|
153
|
+
it "does not include the names of the views" do
|
154
|
+
_(ActiveRecord::Base.connection.tables).wont_include("new_product_users")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe "#views" do
|
159
|
+
it "returns an array of all views" do
|
160
|
+
_(ActiveRecord::Base.connection.views).must_include("new_product_users")
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
146
164
|
if shared_base.supports_materialized_views?
|
147
165
|
describe "#create_materialized_view" do
|
148
166
|
let(:view_name) { :view_name }
|
149
167
|
|
150
168
|
it "throws error when block not given and no build_query" do
|
151
|
-
lambda { shared_base.create_materialized_view(view_name) }.must_raise(RuntimeError)
|
169
|
+
_(lambda { shared_base.create_materialized_view(view_name) }).must_raise(RuntimeError)
|
152
170
|
end
|
153
171
|
|
154
172
|
describe "view_name" do
|
155
173
|
it "takes a symbol as the view_name" do
|
156
|
-
shared_base.create_materialized_view(view_name.to_sym, Product.all).must_match(/#{view_name}/)
|
174
|
+
_(shared_base.create_materialized_view(view_name.to_sym, Product.all)).must_match(/#{view_name}/)
|
157
175
|
end
|
158
176
|
|
159
177
|
it "takes a string as the view_name" do
|
160
|
-
shared_base.create_materialized_view(view_name.to_s, Product.all).must_match(/#{view_name}/)
|
178
|
+
_(shared_base.create_materialized_view(view_name.to_s, Product.all)).must_match(/#{view_name}/)
|
161
179
|
end
|
162
180
|
end
|
163
181
|
|
164
182
|
describe "build_query" do
|
165
183
|
it "uses a string if passed" do
|
166
184
|
select_statement = "SELECT * FROM products"
|
167
|
-
shared_base.create_materialized_view(view_name, select_statement).must_match(/#{Regexp.escape(select_statement)}/)
|
185
|
+
_(shared_base.create_materialized_view(view_name, select_statement)).must_match(/#{Regexp.escape(select_statement)}/)
|
168
186
|
end
|
169
187
|
|
170
188
|
it "uses an Arel::Relation if passed" do
|
171
189
|
select_statement = Product.all.to_sql
|
172
|
-
shared_base.create_materialized_view(view_name, Product.all).must_match(/#{Regexp.escape(select_statement)}/)
|
190
|
+
_(shared_base.create_materialized_view(view_name, Product.all)).must_match(/#{Regexp.escape(select_statement)}/)
|
173
191
|
end
|
174
192
|
end
|
175
193
|
|
176
194
|
describe "block" do
|
177
195
|
it "can use an Arel::Relation from the yield" do
|
178
196
|
select_statement = Product.all.to_sql
|
179
|
-
shared_base.create_materialized_view(view_name) { Product.all }.must_match(/#{Regexp.escape(select_statement)}/)
|
197
|
+
_(shared_base.create_materialized_view(view_name) { Product.all }).must_match(/#{Regexp.escape(select_statement)}/)
|
180
198
|
end
|
181
199
|
|
182
200
|
it "can use a String from the yield" do
|
183
201
|
select_statement = "SELECT * FROM products"
|
184
|
-
shared_base.create_materialized_view(view_name) { "SELECT * FROM products" }.must_match(/#{Regexp.escape(select_statement)}/)
|
202
|
+
_(shared_base.create_materialized_view(view_name) { "SELECT * FROM products" }).must_match(/#{Regexp.escape(select_statement)}/)
|
185
203
|
end
|
186
204
|
end
|
187
205
|
end
|
@@ -191,11 +209,11 @@ shared_examples_for "an adapter" do |adapter|
|
|
191
209
|
|
192
210
|
describe "view_name" do
|
193
211
|
it "takes a symbol as the view_name" do
|
194
|
-
shared_base.drop_materialized_view(view_name.to_sym).must_match(/#{view_name}/)
|
212
|
+
_(shared_base.drop_materialized_view(view_name.to_sym)).must_match(/#{view_name}/)
|
195
213
|
end
|
196
214
|
|
197
215
|
it "takes a string as the view_name" do
|
198
|
-
shared_base.drop_materialized_view(view_name.to_s).must_match(/#{view_name}/)
|
216
|
+
_(shared_base.drop_materialized_view(view_name.to_s)).must_match(/#{view_name}/)
|
199
217
|
end
|
200
218
|
end
|
201
219
|
end
|
@@ -205,18 +223,18 @@ shared_examples_for "an adapter" do |adapter|
|
|
205
223
|
|
206
224
|
describe "view_name" do
|
207
225
|
it "takes a symbol as the view_name" do
|
208
|
-
shared_base.refresh_materialized_view(view_name.to_sym).must_match(/#{view_name}/)
|
226
|
+
_(shared_base.refresh_materialized_view(view_name.to_sym)).must_match(/#{view_name}/)
|
209
227
|
end
|
210
228
|
|
211
229
|
it "takes a string as the view_name" do
|
212
|
-
shared_base.refresh_materialized_view(view_name.to_s).must_match(/#{view_name}/)
|
230
|
+
_(shared_base.refresh_materialized_view(view_name.to_s)).must_match(/#{view_name}/)
|
213
231
|
end
|
214
232
|
end
|
215
233
|
end
|
216
234
|
else
|
217
235
|
describe "#materialized_views" do
|
218
236
|
it "should not be supported by #{adapter}" do
|
219
|
-
lambda { shared_base.materialized_views }.must_raise(NotImplementedError)
|
237
|
+
_(lambda { shared_base.materialized_views }).must_raise(NotImplementedError)
|
220
238
|
end
|
221
239
|
end
|
222
240
|
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
1
|
shared_examples_for "a view model" do
|
4
2
|
ActiveRecord::Base.connection.create_view(:new_product_users) do
|
5
3
|
"SELECT name AS product_name, first_name AS username FROM
|
@@ -12,18 +10,22 @@ shared_examples_for "a view model" do
|
|
12
10
|
|
13
11
|
describe "Spectacles::View" do
|
14
12
|
describe "inherited class" do
|
15
|
-
|
13
|
+
before(:each) do
|
16
14
|
User.destroy_all
|
17
15
|
Product.destroy_all
|
18
16
|
@john = User.create(:first_name => 'John', :last_name => 'Doe')
|
19
17
|
@john.products.create(:name => 'Rubber Duck', :value => 10)
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:new_product_user) { NewProductUser.duck_lovers.load.first }
|
20
21
|
|
21
|
-
|
22
|
+
it "can have scopes" do
|
23
|
+
_(new_product_user.username).must_be @john.first_name
|
22
24
|
end
|
23
25
|
|
24
26
|
describe "an instance" do
|
25
27
|
it "is readonly" do
|
26
|
-
|
28
|
+
_(new_product_user.readonly?).must_be true
|
27
29
|
end
|
28
30
|
end
|
29
31
|
end
|
@@ -48,12 +50,12 @@ shared_examples_for "a view model" do
|
|
48
50
|
MaterializedProductUser.refresh!
|
49
51
|
end
|
50
52
|
|
51
|
-
it "can
|
52
|
-
MaterializedProductUser.duck_lovers.load.first.username.must_be @john.first_name
|
53
|
+
it "can have scopes" do
|
54
|
+
_(MaterializedProductUser.duck_lovers.load.first.username).must_be @john.first_name
|
53
55
|
end
|
54
56
|
|
55
57
|
it "is readonly" do
|
56
|
-
MaterializedProductUser.first.readonly
|
58
|
+
_(MaterializedProductUser.first.readonly?).must_be true
|
57
59
|
end
|
58
60
|
end
|
59
61
|
end
|
data/spectacles.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |gem|
|
|
10
10
|
gem.email = ["liveh2o@gmail.com, brandonsdewitt@gmail.com"]
|
11
11
|
gem.homepage = "http://github.com/liveh2o/spectacles"
|
12
12
|
gem.summary = %q{Spectacles (derived from RailsSQLViews) adds database view functionality to ActiveRecord.}
|
13
|
-
gem.description = %q{Spectacles adds database view functionality to ActiveRecord. Current supported adapters include Postgres, SQLite and
|
13
|
+
gem.description = %q{Spectacles adds database view functionality to ActiveRecord. Current supported adapters include Postgres, SQLite, Vertica, and MySQL.}
|
14
14
|
gem.license = 'MIT'
|
15
15
|
|
16
16
|
gem.files = `git ls-files`.split($\)
|
@@ -21,9 +21,9 @@ Gem::Specification.new do |gem|
|
|
21
21
|
##
|
22
22
|
# Dependencies
|
23
23
|
#
|
24
|
-
gem.required_ruby_version = ">= 2.
|
25
|
-
gem.add_dependency "activerecord", ">= 3.2.0"
|
26
|
-
gem.add_dependency "activesupport", ">= 3.2.0"
|
24
|
+
gem.required_ruby_version = ">= 2.2.0"
|
25
|
+
gem.add_dependency "activerecord", ">= 3.2.0", "< 6.0"
|
26
|
+
gem.add_dependency "activesupport", ">= 3.2.0", "< 6.0"
|
27
27
|
|
28
28
|
##
|
29
29
|
# Development dependencies
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spectacles
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adam Hutchison, Brandon Dewitt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -17,6 +17,9 @@ dependencies:
|
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: 3.2.0
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6.0'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -24,6 +27,9 @@ dependencies:
|
|
24
27
|
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: 3.2.0
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6.0'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: activesupport
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -31,6 +37,9 @@ dependencies:
|
|
31
37
|
- - ">="
|
32
38
|
- !ruby/object:Gem::Version
|
33
39
|
version: 3.2.0
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '6.0'
|
34
43
|
type: :runtime
|
35
44
|
prerelease: false
|
36
45
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -38,6 +47,9 @@ dependencies:
|
|
38
47
|
- - ">="
|
39
48
|
- !ruby/object:Gem::Version
|
40
49
|
version: 3.2.0
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '6.0'
|
41
53
|
- !ruby/object:Gem::Dependency
|
42
54
|
name: rake
|
43
55
|
requirement: !ruby/object:Gem::Requirement
|
@@ -67,7 +79,7 @@ dependencies:
|
|
67
79
|
- !ruby/object:Gem::Version
|
68
80
|
version: '0'
|
69
81
|
description: Spectacles adds database view functionality to ActiveRecord. Current
|
70
|
-
supported adapters include Postgres, SQLite and
|
82
|
+
supported adapters include Postgres, SQLite, Vertica, and MySQL.
|
71
83
|
email:
|
72
84
|
- liveh2o@gmail.com, brandonsdewitt@gmail.com
|
73
85
|
executables: []
|
@@ -89,7 +101,6 @@ files:
|
|
89
101
|
- lib/spectacles/schema_statements.rb
|
90
102
|
- lib/spectacles/schema_statements/abstract_adapter.rb
|
91
103
|
- lib/spectacles/schema_statements/mysql2_adapter.rb
|
92
|
-
- lib/spectacles/schema_statements/mysql_adapter.rb
|
93
104
|
- lib/spectacles/schema_statements/postgresql_adapter.rb
|
94
105
|
- lib/spectacles/schema_statements/sqlite3_adapter.rb
|
95
106
|
- lib/spectacles/schema_statements/sqlserver_adapter.rb
|
@@ -97,7 +108,6 @@ files:
|
|
97
108
|
- lib/spectacles/version.rb
|
98
109
|
- lib/spectacles/view.rb
|
99
110
|
- specs/adapters/mysql2_adapter_spec.rb
|
100
|
-
- specs/adapters/mysql_adapter_spec.rb
|
101
111
|
- specs/adapters/postgresql_adapter_spec.rb
|
102
112
|
- specs/adapters/sqlite3_adapter_spec.rb
|
103
113
|
- specs/spec_helper.rb
|
@@ -121,15 +131,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
121
131
|
requirements:
|
122
132
|
- - ">="
|
123
133
|
- !ruby/object:Gem::Version
|
124
|
-
version: 2.
|
134
|
+
version: 2.2.0
|
125
135
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
136
|
requirements:
|
127
137
|
- - ">="
|
128
138
|
- !ruby/object:Gem::Version
|
129
139
|
version: '0'
|
130
140
|
requirements: []
|
131
|
-
|
132
|
-
rubygems_version: 2.6.12
|
141
|
+
rubygems_version: 3.0.6
|
133
142
|
signing_key:
|
134
143
|
specification_version: 4
|
135
144
|
summary: Spectacles (derived from RailsSQLViews) adds database view functionality
|
@@ -1,39 +0,0 @@
|
|
1
|
-
require 'spectacles/schema_statements/abstract_adapter'
|
2
|
-
|
3
|
-
module Spectacles
|
4
|
-
module SchemaStatements
|
5
|
-
module MysqlAdapter
|
6
|
-
include Spectacles::SchemaStatements::AbstractAdapter
|
7
|
-
|
8
|
-
# overrides the #tables method from ActiveRecord's MysqlAdapter
|
9
|
-
# to return only tables, and not views.
|
10
|
-
def tables(name = nil, database = nil, like = nil)
|
11
|
-
database = database ? quote_table_name(database) : "DATABASE()"
|
12
|
-
by_name = like ? "AND table_name LIKE #{quote(like)}" : ""
|
13
|
-
|
14
|
-
sql = <<-SQL.squish
|
15
|
-
SELECT table_name, table_type
|
16
|
-
FROM information_schema.tables
|
17
|
-
WHERE table_schema = #{database}
|
18
|
-
AND table_type = 'BASE TABLE'
|
19
|
-
#{by_name}
|
20
|
-
SQL
|
21
|
-
|
22
|
-
execute_and_free(sql, 'SCHEMA') do |result|
|
23
|
-
result.collect(&:first)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def views(name = nil) #:nodoc:
|
28
|
-
execute("SHOW FULL TABLES WHERE TABLE_TYPE='VIEW'").map { |row| row[0] }
|
29
|
-
end
|
30
|
-
|
31
|
-
def view_build_query(view, name = nil)
|
32
|
-
row = execute("SHOW CREATE VIEW #{view}", name).first
|
33
|
-
return row[1].gsub(/CREATE .*? (AS)+/i, "")
|
34
|
-
rescue ActiveRecord::StatementInvalid => e
|
35
|
-
raise "No view called #{view} found"
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe "Spectacles::SchemaStatements::MysqlAdapter" do
|
4
|
-
config = {
|
5
|
-
:adapter => "mysql",
|
6
|
-
:host => "localhost",
|
7
|
-
:username => "root"
|
8
|
-
}
|
9
|
-
configure_database(config)
|
10
|
-
recreate_database("spectacles_test")
|
11
|
-
load_schema
|
12
|
-
|
13
|
-
it_behaves_like "an adapter", "MysqlAdapter"
|
14
|
-
it_behaves_like "a view model"
|
15
|
-
end
|