robdimarco_rails_sql_views 0.9.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.
- data/CHANGELOG +22 -0
- data/CONTRIB +8 -0
- data/LICENSE +7 -0
- data/README +51 -0
- data/Rakefile +41 -0
- data/TODO +2 -0
- data/init.rb +1 -0
- data/lib/active_record/view.rb +76 -0
- data/lib/core_ext/module.rb +13 -0
- data/lib/rails_sql_views.rb +51 -0
- data/lib/rails_sql_views/connection_adapters/abstract/schema_definitions.rb +63 -0
- data/lib/rails_sql_views/connection_adapters/abstract/schema_statements.rb +81 -0
- data/lib/rails_sql_views/connection_adapters/abstract_adapter.rb +41 -0
- data/lib/rails_sql_views/connection_adapters/mysql2_adapter.rb +62 -0
- data/lib/rails_sql_views/connection_adapters/mysql_adapter.rb +66 -0
- data/lib/rails_sql_views/connection_adapters/oci_adapter.rb +33 -0
- data/lib/rails_sql_views/connection_adapters/oracle_adapter.rb +33 -0
- data/lib/rails_sql_views/connection_adapters/oracleenhanced_adapter.rb +39 -0
- data/lib/rails_sql_views/connection_adapters/oracleenhanced_adapter.rb.orig +72 -0
- data/lib/rails_sql_views/connection_adapters/postgresql_adapter.rb +65 -0
- data/lib/rails_sql_views/connection_adapters/postgresql_adapter.rb.orig +69 -0
- data/lib/rails_sql_views/connection_adapters/sqlite_adapter.rb +66 -0
- data/lib/rails_sql_views/connection_adapters/sqlserver_adapter.rb +43 -0
- data/lib/rails_sql_views/loader.rb +20 -0
- data/lib/rails_sql_views/schema_dumper.rb +113 -0
- data/lib/rails_sql_views/version.rb +9 -0
- data/rails/init.rb +1 -0
- data/test/README +63 -0
- data/test/adapter_test.rb +82 -0
- data/test/connection.example.yml +12 -0
- data/test/connection/native_mysql/connection.rb +32 -0
- data/test/connection/native_mysql/schema.sql +33 -0
- data/test/connection/native_mysql2/connection.rb +32 -0
- data/test/connection/native_mysql2/schema.sql +33 -0
- data/test/connection/native_postgresql/connection.rb +31 -0
- data/test/connection/native_postgresql/schema.sql +33 -0
- data/test/connection/oracle_enhanced/connection.rb +29 -0
- data/test/connection/oracle_enhanced/procedures.sql +15 -0
- data/test/connection/oracle_enhanced/schema.sql +39 -0
- data/test/models/item.rb +4 -0
- data/test/models/person.rb +5 -0
- data/test/models/person2.rb +3 -0
- data/test/models/place.rb +2 -0
- data/test/models/v_person.rb +4 -0
- data/test/models/v_profile.rb +3 -0
- data/test/schema.native_mysql.expected.rb +57 -0
- data/test/schema.native_mysql2.expected.rb +58 -0
- data/test/schema.native_postgresql.expected.rb +51 -0
- data/test/schema.oracle_enhanced.expected.rb +51 -0
- data/test/schema_dumper_test.rb +130 -0
- data/test/test_helper.rb +30 -0
- data/test/view_model_test.rb +63 -0
- data/test/view_operations_test.rb +36 -0
- metadata +246 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
module RailsSqlViews
|
2
|
+
module ConnectionAdapters
|
3
|
+
module SQLiteAdapter
|
4
|
+
def supports_views?
|
5
|
+
true
|
6
|
+
end
|
7
|
+
|
8
|
+
def supports_drop_table_cascade?
|
9
|
+
return false
|
10
|
+
end
|
11
|
+
|
12
|
+
def tables(name = nil) #:nodoc:
|
13
|
+
sql = <<-SQL
|
14
|
+
SELECT name
|
15
|
+
FROM sqlite_master
|
16
|
+
WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence'
|
17
|
+
SQL
|
18
|
+
|
19
|
+
execute(sql, name).map do |row|
|
20
|
+
row[0]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def base_tables(name = nil)
|
25
|
+
sql = <<-SQL
|
26
|
+
SELECT name
|
27
|
+
FROM sqlite_master
|
28
|
+
WHERE (type = 'table') AND NOT name = 'sqlite_sequence'
|
29
|
+
SQL
|
30
|
+
|
31
|
+
execute(sql, name).map do |row|
|
32
|
+
row[0]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
alias nonview_tables base_tables
|
36
|
+
|
37
|
+
def views(name = nil)
|
38
|
+
sql = <<-SQL
|
39
|
+
SELECT name
|
40
|
+
FROM sqlite_master
|
41
|
+
WHERE type = 'view' AND NOT name = 'sqlite_sequence'
|
42
|
+
SQL
|
43
|
+
|
44
|
+
execute(sql, name).map do |row|
|
45
|
+
row[0]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get the view select statement for the specified table.
|
50
|
+
def view_select_statement(view, name = nil)
|
51
|
+
sql = <<-SQL
|
52
|
+
SELECT sql
|
53
|
+
FROM sqlite_master
|
54
|
+
WHERE name = '#{view}' AND NOT name = 'sqlite_sequence'
|
55
|
+
SQL
|
56
|
+
|
57
|
+
(select_value(sql, name).gsub("CREATE VIEW #{view} AS ", "")) or raise "No view called #{view} found"
|
58
|
+
end
|
59
|
+
|
60
|
+
def supports_view_columns_definition?
|
61
|
+
false
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module RailsSqlViews
|
2
|
+
module ConnectionAdapters
|
3
|
+
module SQLServerAdapter
|
4
|
+
# Returns true as this adapter supports views.
|
5
|
+
def supports_views?
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
# Get all of the non-view tables from the currently connected schema
|
10
|
+
def base_tables(name = nil)
|
11
|
+
# this is untested
|
12
|
+
select_values("SELECT table_name FROM information_schema.tables", name)
|
13
|
+
end
|
14
|
+
alias nonview_tables base_tables
|
15
|
+
|
16
|
+
# Returns all the view names from the currently connected schema.
|
17
|
+
def views(name = nil)
|
18
|
+
select_values("SELECT table_name FROM information_schema.views", name)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get the view select statement for the specified view.
|
22
|
+
def view_select_statement(view, name=nil)
|
23
|
+
q =<<-ENDSQL
|
24
|
+
SELECT view_definition FROM information_schema.views
|
25
|
+
WHERE table_name = '#{view}'
|
26
|
+
ENDSQL
|
27
|
+
|
28
|
+
view_def = select_value(q, name)
|
29
|
+
|
30
|
+
if view_def
|
31
|
+
return convert_statement(view_def)
|
32
|
+
else
|
33
|
+
raise "No view called #{view} found"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def convert_statement(s)
|
39
|
+
s.sub(/^CREATE.* AS (select .*)/i, '\1').gsub(/\n/, '')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
module RailsSqlViews
|
3
|
+
module Loader
|
4
|
+
SUPPORTED_ADAPTERS = %w( Mysql Mysql2 PostgreSQL SQLServer SQLite OracleEnhanced )
|
5
|
+
|
6
|
+
def self.load_extensions
|
7
|
+
SUPPORTED_ADAPTERS.each do |db|
|
8
|
+
if ActiveRecord::ConnectionAdapters.const_defined?("#{db}Adapter")
|
9
|
+
require "rails_sql_views/connection_adapters/#{db.downcase}_adapter"
|
10
|
+
ActiveRecord::ConnectionAdapters.const_get("#{db}Adapter").class_eval do
|
11
|
+
include RailsSqlViews::ConnectionAdapters::AbstractAdapter
|
12
|
+
include RailsSqlViews::ConnectionAdapters.const_get("#{db}Adapter")
|
13
|
+
# prevent reloading extension when the environment is reloaded
|
14
|
+
$rails_sql_views_included = true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module RailsSqlViews
|
2
|
+
module SchemaDumper
|
3
|
+
def self.included(base)
|
4
|
+
base.alias_method_chain :trailer, :views
|
5
|
+
base.alias_method_chain :dump, :views
|
6
|
+
base.alias_method_chain :tables, :views_excluded
|
7
|
+
|
8
|
+
# A list of views which should not be dumped to the schema.
|
9
|
+
# Acceptable values are strings as well as regexp.
|
10
|
+
# This setting is only used if ActiveRecord::Base.schema_format == :ruby
|
11
|
+
base.cattr_accessor :ignore_views
|
12
|
+
base.ignore_views = []
|
13
|
+
# Optional: specify the order that in which views are created.
|
14
|
+
# This allows views to depend on and include fields from other views.
|
15
|
+
# It is not necessary to specify all the view names, just the ones that
|
16
|
+
# need to be created first
|
17
|
+
base.cattr_accessor :view_creation_order
|
18
|
+
base.view_creation_order = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def trailer_with_views(stream)
|
22
|
+
# do nothing...we'll call this later
|
23
|
+
end
|
24
|
+
|
25
|
+
# Add views to the end of the dump stream
|
26
|
+
def dump_with_views(stream)
|
27
|
+
dump_without_views(stream)
|
28
|
+
begin
|
29
|
+
@connection.class.send(:public, :supports_views?) # this is a kluge, but I don't know why it's changing back to private before it gets here
|
30
|
+
if @connection.supports_views?
|
31
|
+
views(stream)
|
32
|
+
end
|
33
|
+
rescue => e
|
34
|
+
if ActiveRecord::Base.logger
|
35
|
+
ActiveRecord::Base.logger.error "Unable to dump views: #{e}"
|
36
|
+
else
|
37
|
+
raise e
|
38
|
+
end
|
39
|
+
end
|
40
|
+
trailer_without_views(stream)
|
41
|
+
stream
|
42
|
+
end
|
43
|
+
|
44
|
+
# Add views to the stream
|
45
|
+
def views(stream)
|
46
|
+
if view_creation_order.empty?
|
47
|
+
sorted_views = @connection.views.sort
|
48
|
+
else
|
49
|
+
# set union, merge by joining arrays, removing dups
|
50
|
+
# this will float the view name sin view_creation_order to the top
|
51
|
+
# without requiring all the views to be specified
|
52
|
+
sorted_views = view_creation_order | @connection.views
|
53
|
+
end
|
54
|
+
sorted_views.each do |v|
|
55
|
+
next if [ActiveRecord::Migrator.schema_migrations_table_name, ignore_views].flatten.any? do |ignored|
|
56
|
+
case ignored
|
57
|
+
when String then v == ignored
|
58
|
+
when Symbol then v == ignored.to_s
|
59
|
+
when Regexp then v =~ ignored
|
60
|
+
else
|
61
|
+
raise StandardError, 'ActiveRecord::SchemaDumper.ignore_views accepts an array of String and / or Regexp values.'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
view(v, stream)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Add the specified view to the stream
|
69
|
+
def view(view, stream)
|
70
|
+
columns = @connection.columns(view).collect { |c| c.name }
|
71
|
+
begin
|
72
|
+
v = StringIO.new
|
73
|
+
|
74
|
+
v.print " create_view #{view.inspect}"
|
75
|
+
v.print ", #{@connection.view_select_statement(view).dump}"
|
76
|
+
v.print ", :force => true"
|
77
|
+
v.puts " do |v|"
|
78
|
+
|
79
|
+
columns.each do |column|
|
80
|
+
v.print " v.column :#{column}"
|
81
|
+
v.puts
|
82
|
+
end
|
83
|
+
|
84
|
+
v.puts " end"
|
85
|
+
v.puts
|
86
|
+
|
87
|
+
v.rewind
|
88
|
+
stream.print v.read
|
89
|
+
rescue => e
|
90
|
+
stream.puts "# Could not dump view #{view.inspect} because of following #{e.class}"
|
91
|
+
stream.puts "# #{e.message}"
|
92
|
+
stream.puts
|
93
|
+
end
|
94
|
+
|
95
|
+
stream
|
96
|
+
end
|
97
|
+
|
98
|
+
def tables_with_views_excluded(stream)
|
99
|
+
@connection.base_tables.sort.each do |tbl|
|
100
|
+
next if [ActiveRecord::Migrator.schema_migrations_table_name, ignore_tables].flatten.any? do |ignored|
|
101
|
+
case ignored
|
102
|
+
when String then tbl == ignored
|
103
|
+
when Regexp then tbl =~ ignored
|
104
|
+
else
|
105
|
+
raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
table(tbl, stream)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'rails_sql_views'
|
data/test/README
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
== Executing Tests
|
2
|
+
|
3
|
+
First create the test database: rails_sql_views_unittest
|
4
|
+
|
5
|
+
Next, copy test/connection.example.yml to test/connection.yml and change the
|
6
|
+
settings to match your database configuration that you are testing.
|
7
|
+
|
8
|
+
To run the tests either use:
|
9
|
+
|
10
|
+
rake
|
11
|
+
|
12
|
+
Which will run the MySQL tests, or use something like the following:
|
13
|
+
|
14
|
+
rake DB=native_postgresql
|
15
|
+
|
16
|
+
Substituting the directory name inside the connection directory.
|
17
|
+
|
18
|
+
== Status
|
19
|
+
|
20
|
+
Currently the following adapters have been tested:
|
21
|
+
|
22
|
+
* MySQL
|
23
|
+
* PostgreSQL Pure Ruby
|
24
|
+
|
25
|
+
The Oracle and SQL Server adapters have not been tested by me.
|
26
|
+
|
27
|
+
== Implementing Adapters
|
28
|
+
|
29
|
+
If you would like to implement an adapter, it should go in
|
30
|
+
lib/rails_sql_views/connection_adapters. Follow the conventions
|
31
|
+
of the other adapters currently implemented. Every adapter must implement the
|
32
|
+
following methods:
|
33
|
+
|
34
|
+
supports_views?
|
35
|
+
base_tables(name = nil)
|
36
|
+
views(name = nil)
|
37
|
+
view_select_statement(view, name=nil)
|
38
|
+
|
39
|
+
The suports_views? method must return true. The views method must return an
|
40
|
+
array of all view names. The view_select_statement method must return the
|
41
|
+
select statement used to construct the specified view.
|
42
|
+
|
43
|
+
In addition you must include the following for testing purposes:
|
44
|
+
|
45
|
+
The script which establishes the database connection in ActiveRecord:
|
46
|
+
|
47
|
+
test/connection/driver_name/connection.rb
|
48
|
+
|
49
|
+
The schema to setup the test database in your drivers native form:
|
50
|
+
|
51
|
+
test/connection/driver_name/schema.sql
|
52
|
+
|
53
|
+
The expected schema output from a schema dump. Note that it must be formatted
|
54
|
+
*exactly* as the output would be:
|
55
|
+
|
56
|
+
test/schema.driver_name.out.rb
|
57
|
+
|
58
|
+
Once this is done you should send the diff of the changes to
|
59
|
+
anthonyeden@gmail.com. Any questions can also be emailed to this address.
|
60
|
+
|
61
|
+
== Known Issues
|
62
|
+
|
63
|
+
* If you are running on Rails 1.1.6 then the schema dumper test will fail because the formatting of the schema output has changed between 1.1.6 and 1.2.
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/test_helper')
|
2
|
+
|
3
|
+
class AdapterTest < Test::Unit::TestCase
|
4
|
+
def test_current_database
|
5
|
+
if ActiveRecord::Base.connection.respond_to?(:current_database)
|
6
|
+
assert_equal 'rails_sql_views_unittest', ActiveRecord::Base.connection.current_database
|
7
|
+
end
|
8
|
+
end
|
9
|
+
def test_tables
|
10
|
+
create_view
|
11
|
+
found = ActiveRecord::Base.connection.tables.sort
|
12
|
+
found.delete(ActiveRecord::Migrator.schema_migrations_table_name)
|
13
|
+
assert_equal ["items", "items_people", "people", "people2", "places", "v_people"], found
|
14
|
+
end
|
15
|
+
def test_base_tables
|
16
|
+
create_view
|
17
|
+
found = ActiveRecord::Base.connection.base_tables.sort
|
18
|
+
found.delete(ActiveRecord::Migrator.schema_migrations_table_name)
|
19
|
+
assert_equal ["items", "items_people", "people", "people2", "places"], found
|
20
|
+
end
|
21
|
+
def test_views
|
22
|
+
create_view
|
23
|
+
assert_equal ['v_people'], ActiveRecord::Base.connection.views
|
24
|
+
end
|
25
|
+
def test_columns
|
26
|
+
create_view
|
27
|
+
assert_equal ["f_name", "l_name", "social_security"], ActiveRecord::Base.connection.columns('v_people').collect { |c| c.name }
|
28
|
+
end
|
29
|
+
def test_supports_views
|
30
|
+
assert ActiveRecord::Base.connection.supports_views?
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_mapped_views
|
34
|
+
create_mapping
|
35
|
+
assert_equal ['v_people'], ActiveRecord::Base.connection.views
|
36
|
+
end
|
37
|
+
def test_mapped_columns
|
38
|
+
create_mapping
|
39
|
+
assert_equal ["f_name", "l_name", "address_id"], ActiveRecord::Base.connection.columns('v_people').collect { |c| c.name }
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_view_select_statement
|
43
|
+
case ActiveRecord::Base.connection.adapter_name
|
44
|
+
when "MySQL"
|
45
|
+
assert_equal "select `people`.`first_name` AS `f_name`,`people`.`last_name` AS `l_name`,`people`.`ssn` AS `social_security` from `people`", ActiveRecord::Base.connection.view_select_statement('v_people')
|
46
|
+
when "PostgreSQL"
|
47
|
+
assert_equal "SELECT people.first_name AS f_name, people.last_name AS l_name, people.ssn AS social_security FROM people;", ActiveRecord::Base.connection.view_select_statement('v_people')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_old_name_not_found_error_during_mapping
|
52
|
+
assert_raise ActiveRecord::ActiveRecordError do
|
53
|
+
ActiveRecord::Base.connection.create_mapping_view(:people, :v_people, :force => true) do |v|
|
54
|
+
v.map_column :foo, :bar
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
### TODO
|
60
|
+
# def test_only_base_table_triggers_are_dropped_for_disabled_ref_integrity
|
61
|
+
# ActiveRecord::Base.connection.disable_referential_integrity do
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
|
65
|
+
private
|
66
|
+
def create_view
|
67
|
+
ActiveRecord::Base.connection.create_view(:v_people, 'select first_name, last_name, ssn from people', :force => true) do |v|
|
68
|
+
v.column :f_name
|
69
|
+
v.column :l_name
|
70
|
+
v.column :social_security
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def create_mapping
|
75
|
+
ActiveRecord::Base.connection.create_mapping_view(:people, :v_people, :force => true) do |v|
|
76
|
+
v.map_column :id, nil
|
77
|
+
v.map_column :first_name, :f_name
|
78
|
+
v.map_column :last_name, :l_name
|
79
|
+
v.map_column :ssn, nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
print "Using native MySQL\n"
|
2
|
+
|
3
|
+
adapter_name = 'mysql'
|
4
|
+
config = YAML.load_file(File.join(File.dirname(__FILE__), '/../../connection.yml'))[adapter_name]
|
5
|
+
|
6
|
+
#require 'logger'
|
7
|
+
#ActiveRecord::Base.logger = Logger.new("debug.log")
|
8
|
+
|
9
|
+
ActiveRecord::Base.silence do
|
10
|
+
ActiveRecord::Base.configurations = {
|
11
|
+
config['database'] => {
|
12
|
+
:adapter => adapter_name,
|
13
|
+
:username => config['username'],
|
14
|
+
:password => config['password'],
|
15
|
+
:host => config['host'],
|
16
|
+
:database => config['database'],
|
17
|
+
:encoding => config['encoding'],
|
18
|
+
:schema_file => config['schema_file'],
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
ActiveRecord::Base.establish_connection config['database']
|
23
|
+
ActiveRecord::Migration.verbose = false
|
24
|
+
|
25
|
+
puts "Resetting database"
|
26
|
+
conn = ActiveRecord::Base.connection
|
27
|
+
conn.recreate_database(conn.current_database)
|
28
|
+
conn.reconnect!
|
29
|
+
lines = open(File.join(File.dirname(__FILE__), ActiveRecord::Base.configurations[config['database']][:schema_file])).readlines
|
30
|
+
lines.join.split(';').each { |line| conn.execute(line) }
|
31
|
+
conn.reconnect!
|
32
|
+
end
|