robdimarco_rails_sql_views 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/CHANGELOG +22 -0
  2. data/CONTRIB +8 -0
  3. data/LICENSE +7 -0
  4. data/README +51 -0
  5. data/Rakefile +41 -0
  6. data/TODO +2 -0
  7. data/init.rb +1 -0
  8. data/lib/active_record/view.rb +76 -0
  9. data/lib/core_ext/module.rb +13 -0
  10. data/lib/rails_sql_views.rb +51 -0
  11. data/lib/rails_sql_views/connection_adapters/abstract/schema_definitions.rb +63 -0
  12. data/lib/rails_sql_views/connection_adapters/abstract/schema_statements.rb +81 -0
  13. data/lib/rails_sql_views/connection_adapters/abstract_adapter.rb +41 -0
  14. data/lib/rails_sql_views/connection_adapters/mysql2_adapter.rb +62 -0
  15. data/lib/rails_sql_views/connection_adapters/mysql_adapter.rb +66 -0
  16. data/lib/rails_sql_views/connection_adapters/oci_adapter.rb +33 -0
  17. data/lib/rails_sql_views/connection_adapters/oracle_adapter.rb +33 -0
  18. data/lib/rails_sql_views/connection_adapters/oracleenhanced_adapter.rb +39 -0
  19. data/lib/rails_sql_views/connection_adapters/oracleenhanced_adapter.rb.orig +72 -0
  20. data/lib/rails_sql_views/connection_adapters/postgresql_adapter.rb +65 -0
  21. data/lib/rails_sql_views/connection_adapters/postgresql_adapter.rb.orig +69 -0
  22. data/lib/rails_sql_views/connection_adapters/sqlite_adapter.rb +66 -0
  23. data/lib/rails_sql_views/connection_adapters/sqlserver_adapter.rb +43 -0
  24. data/lib/rails_sql_views/loader.rb +20 -0
  25. data/lib/rails_sql_views/schema_dumper.rb +113 -0
  26. data/lib/rails_sql_views/version.rb +9 -0
  27. data/rails/init.rb +1 -0
  28. data/test/README +63 -0
  29. data/test/adapter_test.rb +82 -0
  30. data/test/connection.example.yml +12 -0
  31. data/test/connection/native_mysql/connection.rb +32 -0
  32. data/test/connection/native_mysql/schema.sql +33 -0
  33. data/test/connection/native_mysql2/connection.rb +32 -0
  34. data/test/connection/native_mysql2/schema.sql +33 -0
  35. data/test/connection/native_postgresql/connection.rb +31 -0
  36. data/test/connection/native_postgresql/schema.sql +33 -0
  37. data/test/connection/oracle_enhanced/connection.rb +29 -0
  38. data/test/connection/oracle_enhanced/procedures.sql +15 -0
  39. data/test/connection/oracle_enhanced/schema.sql +39 -0
  40. data/test/models/item.rb +4 -0
  41. data/test/models/person.rb +5 -0
  42. data/test/models/person2.rb +3 -0
  43. data/test/models/place.rb +2 -0
  44. data/test/models/v_person.rb +4 -0
  45. data/test/models/v_profile.rb +3 -0
  46. data/test/schema.native_mysql.expected.rb +57 -0
  47. data/test/schema.native_mysql2.expected.rb +58 -0
  48. data/test/schema.native_postgresql.expected.rb +51 -0
  49. data/test/schema.oracle_enhanced.expected.rb +51 -0
  50. data/test/schema_dumper_test.rb +130 -0
  51. data/test/test_helper.rb +30 -0
  52. data/test/view_model_test.rb +63 -0
  53. data/test/view_operations_test.rb +36 -0
  54. metadata +246 -0
data/CHANGELOG ADDED
@@ -0,0 +1,22 @@
1
+ 0.1.0 - Released Dec 27, 2006
2
+ * Initial release
3
+
4
+ 0.5.0 - Released Dec 29, 2006
5
+ * Added support for PostgreSQL (Michael Schuerig)
6
+ * Fixed the schema dumper
7
+
8
+ 0.5.1 - Released Jan 10, 2007
9
+ * Patch by Clifford T. Matthews to use String.dump to dump out the view select statement
10
+
11
+ 0.6.0 - Released May 9, 2007
12
+ * Added support for SQL Server (Seth Ladd)-
13
+ * Added support for using views to map non-friendly database field names to AR-friendly names (Nathan Vack)
14
+ * Added support for Oracle (Alistair Davidson)
15
+
16
+ 0.6.1 - Released June 6, 2007
17
+ * Added test for union support
18
+ * Updated tests to include new table to support union test
19
+
20
+ 0.7.0
21
+ * Updated dependency versions to get on ActiveRecord 2.x. May not be compatible with Rails versions less than 2.x.
22
+ * Add nonview_tables support to *all* of the adapters (was missing from Postgres and SQLServer)
data/CONTRIB ADDED
@@ -0,0 +1,8 @@
1
+ The following people have kindly contributed to improve Rails SQL Views:
2
+
3
+ Anthony Eden
4
+ Michael Schuerig
5
+ Clifford T. Matthews
6
+ Seth Ladd
7
+
8
+ So if you see them in the halls of your local Ruby or Rails gatherings please remember to say Hi and Thanks. ;-)
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2006-2008 Anthony Eden
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,51 @@
1
+ == Rails SQL Views
2
+
3
+ Library which adds SQL Views to Rails. Adds create_view and drop_view to the ActiveRecord::ConnectionAdapters::AbstractAdapter (which makes them available to migrations) and adds support for dumping views in the ActiveRecord::SchemaDumper.
4
+
5
+ == Installation
6
+
7
+ To install:
8
+
9
+ gem install rails_sql_views
10
+
11
+ Then add the following to your Rails config/environment.rb:
12
+
13
+ require_gem 'rails_sql_views'
14
+ require 'rails_sql_views'
15
+
16
+ == Usage
17
+
18
+ You can then use create_view and drop_view in your migrations. For example:
19
+
20
+ class CreatePersonView < ActiveRecord::Migration
21
+ def self.up
22
+ create_view :v_people, "select * from people" do |t|
23
+ t.column :id
24
+ t.column :name
25
+ t.column :social_security
26
+ end
27
+ end
28
+
29
+ def self.down
30
+ drop_view :v_people
31
+ end
32
+ end
33
+
34
+ This extension also adds support for views in the ActiveRecord::SchemaDumper class.
35
+
36
+ The following drivers are supported:
37
+
38
+ MySQL
39
+ PostgreSQL (Native and Pure Ruby)
40
+ Oracle
41
+ SQL Server
42
+
43
+ == Known Issues
44
+
45
+ * Drivers not mentioned above are not supported.
46
+
47
+ If you find any issues please send an email to anthonyeden@gmail.com .
48
+
49
+ == Contributing
50
+
51
+ If you would like to implement view support for other adapters then please drop me an email. Better yet, write up the adapter modifications and send them to me. :-)
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env rake
2
+ #
3
+ # Development tasks
4
+
5
+ begin
6
+ require 'bundler/setup'
7
+ rescue LoadError
8
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
9
+ end
10
+
11
+ begin
12
+ require 'rdoc/task'
13
+ rescue LoadError
14
+ require 'rdoc/rdoc'
15
+ require 'rake/rdoctask'
16
+ RDoc::Task = Rake::RDocTask
17
+ end
18
+
19
+ # Documentation
20
+ RDoc::Task.new(:rdoc) do |rdoc|
21
+ rdoc.rdoc_dir = 'rdoc'
22
+ rdoc.title = 'RailsSqlViews'
23
+ rdoc.options << '--line-numbers'
24
+ rdoc.rdoc_files.include('README.rdoc')
25
+ rdoc.rdoc_files.include('lib/**/*.rb')
26
+ end
27
+
28
+ # Gem building
29
+ Bundler::GemHelper.install_tasks
30
+
31
+ require 'rake/testtask'
32
+
33
+ Rake::TestTask.new(:test) do |t|
34
+ t.libs << 'lib'
35
+ t.libs << 'test'
36
+ t.pattern = 'test/**/*_test.rb'
37
+ t.verbose = false
38
+ end
39
+
40
+
41
+ task :default => ['test', 'release']
data/TODO ADDED
@@ -0,0 +1,2 @@
1
+
2
+ - Use pre-existing Adapter#supports_views? where possible
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'rails_sql_views'
@@ -0,0 +1,76 @@
1
+
2
+ # A base class for database views.
3
+ # It is primarily useful for views that are centered around a single table/model.
4
+ module ActiveRecord # :nodoc:
5
+ class View < Base
6
+ self.abstract_class = true
7
+
8
+ def readonly?
9
+ true
10
+ end
11
+
12
+ class << self
13
+ # Clones all applicable associations from +model+ to this view
14
+ # and provides an instance method
15
+ # <tt>to_<em>model</em></tt>
16
+ # that casts a view object to an object of the kind view is
17
+ # based on. This latter object may be missing attributes; to fill
18
+ # them in, call #reload.
19
+ def based_on(model)
20
+ define_method("to_#{model.name.demodulize.underscore}") do
21
+ becomes(model)
22
+ end
23
+
24
+ model.reflect_on_all_associations.each do |assoc|
25
+ clone_association(model, assoc)
26
+ end
27
+ end
28
+
29
+ # Clone one or more associations from +model+ to this view class.
30
+ #
31
+ # NOTE: Currently only <tt>belongs_to</tt>, <tt>has_many</tt> (withouth
32
+ # <tt>:through</tt>), and <tt>has_and_belongs_to_many</tt> associations
33
+ # are supported.
34
+ def clone_association(model, *associations)
35
+ associations.each do |association|
36
+ r = case association
37
+ when String, Symbol
38
+ model.reflect_on_association(association.to_sym)
39
+ when ActiveRecord::Reflection::AssociationReflection
40
+ association
41
+ else
42
+ raise ArgumentError, "Unrecognized association #{association.inspect}; must be a Symbol, String, or AssociationReflection."
43
+ end
44
+ case r.macro
45
+ when :belongs_to
46
+ if self.column_names.include?(r.primary_key_name.to_s)
47
+ if !r.options[:foreign_type] || self.column_names.include?(r.options[:foreign_type])
48
+ options = r.options.merge(
49
+ :class_name => r.class_name,
50
+ :foreign_key => r.primary_key_name
51
+ )
52
+ belongs_to r.name, options
53
+ end
54
+ end
55
+ when :has_many
56
+ ### TODO :through assocications
57
+ options = r.options.merge(
58
+ :class_name => r.class_name,
59
+ :foreign_key => r.primary_key_name
60
+ )
61
+ has_many r.name, options
62
+ when :has_and_belongs_to_many
63
+ options = r.options.merge(
64
+ :class_name => r.class_name,
65
+ :foreign_key => r.primary_key_name,
66
+ :association_foreign_key => r.association_foreign_key
67
+ )
68
+ has_and_belongs_to_many r.name, options
69
+ when :has_one
70
+ ### TODO
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,13 @@
1
+ # This is required for 1.1.6 support
2
+ unless Module.respond_to?(:alias_method_chain)
3
+ class Module
4
+ def alias_method_chain(target, feature)
5
+ # Strip out punctuation on predicates or bang methods since
6
+ # e.g. target?_without_feature is not a valid method name.
7
+ aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
8
+ yield(aliased_target, punctuation) if block_given?
9
+ alias_method "#{aliased_target}_without_#{feature}#{punctuation}", target
10
+ alias_method target, "#{aliased_target}_with_#{feature}#{punctuation}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,51 @@
1
+ #--
2
+ # Copyright (c) 2006 Anthony Eden
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ $:.unshift(File.dirname(__FILE__))
25
+
26
+ require 'active_record'
27
+
28
+ require 'core_ext/module'
29
+
30
+ require 'rails_sql_views/connection_adapters/abstract/schema_definitions'
31
+ require 'rails_sql_views/connection_adapters/abstract/schema_statements'
32
+ require 'rails_sql_views/connection_adapters/abstract_adapter'
33
+ require 'rails_sql_views/schema_dumper'
34
+ require 'rails_sql_views/loader'
35
+
36
+ $rails_sql_views_included = false
37
+
38
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
39
+ include RailsSqlViews::ConnectionAdapters::SchemaStatements
40
+ def self.inherited(sub)
41
+ unless $rails_sql_views_included && (Rails.env.test? || Rails.env.cucumber?)
42
+ RailsSqlViews::Loader.load_extensions
43
+ end
44
+ end
45
+ end
46
+
47
+ ActiveRecord::SchemaDumper.class_eval do
48
+ include RailsSqlViews::SchemaDumper
49
+ end
50
+
51
+ RailsSqlViews::Loader.load_extensions
@@ -0,0 +1,63 @@
1
+ module RailsSqlViews
2
+ module ConnectionAdapters #:nodoc:
3
+ # Abstract definition of a View
4
+ class ViewDefinition
5
+ attr_accessor :columns, :select_query
6
+
7
+ def initialize(base, select_query)
8
+ @columns = []
9
+ @base = base
10
+ @select_query = select_query
11
+ end
12
+
13
+ def column(name)
14
+ column = name.to_s
15
+ @columns << column unless @columns.include? column
16
+ self
17
+ end
18
+
19
+ def to_sql
20
+ @columns.collect { |c| @base.quote_column_name(c) } * ', '
21
+ end
22
+
23
+ end
24
+
25
+ class MappingDefinition
26
+
27
+ # Generates a hash of the form :old_column => :new_column
28
+ # Initially, it'll map column names to themselves.
29
+ # use map_column to modify the list.
30
+ def initialize(columns)
31
+ @columns = columns
32
+ @map = Hash.new()
33
+ columns.each do |c|
34
+ @map[c] = c
35
+ end
36
+
37
+ end
38
+
39
+ # Create a mapping from an old column name to a new one.
40
+ # If the new name is nil, specify that the old column shouldn't
41
+ # appear in this new view.
42
+ def map_column(old_name, new_name)
43
+ unless @map.include?(old_name)
44
+ raise ActiveRecord::ActiveRecordError, "column #{old_name} not found, can't be mapped"
45
+ end
46
+ if new_name.nil?
47
+ @map.delete old_name
48
+ @columns.delete old_name
49
+ else
50
+ @map[old_name] = new_name
51
+ end
52
+ end
53
+
54
+ def select_cols
55
+ @columns
56
+ end
57
+
58
+ def view_cols
59
+ @columns.map { |c| @map[c] }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,81 @@
1
+ module RailsSqlViews
2
+ module ConnectionAdapters # :nodoc:
3
+ module SchemaStatements
4
+ def self.included(base)
5
+ base.alias_method_chain :drop_table, :cascade
6
+ end
7
+
8
+ # Create a view.
9
+ # The +options+ hash can include the following keys:
10
+ # [<tt>:check_option</tt>]
11
+ # Specify restrictions for inserts or updates in updatable views. ANSI SQL 92 defines two check option
12
+ # values: CASCADED and LOCAL. See your database documentation for allowed values.
13
+ # [<tt>:replace</tt>]
14
+ # Specify we should use CREATE OR REPLACE VIEW not just CREATE VIEW
15
+ def create_view(name, select_query, options={})
16
+ if supports_views?
17
+ view_definition = ViewDefinition.new(self, select_query)
18
+
19
+ if block_given?
20
+ yield view_definition
21
+ end
22
+
23
+ if options[:force]
24
+ drop_view(name) rescue nil
25
+ end
26
+
27
+ create_sql = options[:replace] ? "CREATE OR REPLACE VIEW " : "CREATE VIEW "
28
+ create_sql << "#{quote_table_name(name)} "
29
+ if supports_view_columns_definition? && !view_definition.to_sql.blank?
30
+ create_sql << "("
31
+ create_sql << view_definition.to_sql
32
+ create_sql << ") "
33
+ end
34
+ create_sql << "AS #{view_definition.select_query}"
35
+ create_sql << " WITH #{options[:check_option]} CHECK OPTION" if options[:check_option]
36
+ execute create_sql
37
+ end
38
+ end
39
+
40
+ # Also creates a view, with the specific purpose of remapping column names
41
+ # to make non-ActiveRecord tables friendly with the naming
42
+ # conventions, while maintaining legacy app compatibility.
43
+ def create_mapping_view(old_name, new_name, options = {})
44
+ return unless supports_views?
45
+
46
+ col_names = columns(old_name).collect { |col| col.name.to_sym }
47
+ mapper = MappingDefinition.new(col_names)
48
+
49
+ yield mapper
50
+
51
+ if options[:force]
52
+ drop_view(new_name) rescue nil
53
+ end
54
+
55
+ view_sql = "CREATE VIEW #{new_name} "
56
+ if supports_view_columns_definition?
57
+ view_sql << "(#{mapper.view_cols.collect { |c| quote_column_name(c) }.join(', ')}) "
58
+ end
59
+ view_sql << "AS SELECT #{mapper.select_cols.collect { |c| quote_column_name(c) }.join(', ')} FROM #{old_name}"
60
+ execute view_sql
61
+ end
62
+
63
+ def drop_table_with_cascade(table_name, options = {})
64
+ execute "DROP TABLE #{quote_table_name(table_name)} CASCADE"
65
+ end
66
+
67
+ # Drop a view.
68
+ # The +options+ hash can include the following keys:
69
+ # [<tt>:drop_behavior</tt>]
70
+ # Specify the drop behavior. ANSI SQL 92 defines two drop behaviors, CASCADE and RESTRICT. See your
71
+ # database documentation to determine what drop behaviors are available.
72
+ def drop_view(name, options={})
73
+ if supports_views?
74
+ drop_sql = "DROP VIEW #{quote_table_name(name)}"
75
+ drop_sql << " #{options[:drop_behavior]}" if options[:drop_behavior]
76
+ execute drop_sql
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,41 @@
1
+ module RailsSqlViews
2
+ module ConnectionAdapters
3
+ module AbstractAdapter
4
+ def self.included(base)
5
+ base.alias_method_chain :disable_referential_integrity, :views_excluded
6
+ end
7
+
8
+ # Subclasses should override and return true if they support views.
9
+ def supports_views?
10
+ return false
11
+ end
12
+
13
+ # Subclasses should override and return false if they don't support CASCADE
14
+ def supports_drop_table_cascade?
15
+ return true
16
+ end
17
+
18
+ def disable_referential_integrity_with_views_excluded(&block)
19
+ self.class.send(:alias_method, :original_tables_method, :tables)
20
+ self.class.send(:alias_method, :tables, :base_tables)
21
+ disable_referential_integrity_without_views_excluded(&block)
22
+ ensure
23
+ self.class.send(:alias_method, :tables, :original_tables_method)
24
+ end
25
+
26
+ def supports_view_columns_definition?
27
+ true
28
+ end
29
+
30
+ # Get a list of all views for the current database
31
+ def views(name = nil)
32
+ raise NotImplementedError, "views is an abstract method"
33
+ end
34
+
35
+ # Get the select statement for the specified view
36
+ def view_select_statement(view, name=nil)
37
+ raise NotImplementedError, "view_select_statement is an abstract method"
38
+ end
39
+ end
40
+ end
41
+ end