diff --git a/.gitignore b/.gitignore index cf8d5b5d1..f623309f9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ log tmp sqlnet.log Gemfile.lock +/spec/spec_config.yaml diff --git a/Gemfile b/Gemfile index 7f75ea331..86ec4ef91 100644 --- a/Gemfile +++ b/Gemfile @@ -1,17 +1,17 @@ source 'http://rubygems.org' group :development do - gem 'jeweler', '~> 1.8' + gem 'jeweler', '~> 2.0' gem 'rspec', '~> 2.4' gem 'rdoc' - gem 'activerecord', github: 'rails/rails' - gem 'activemodel', github: 'rails/rails' - gem 'activesupport', github: 'rails/rails' - gem 'actionpack', github: 'rails/rails' - gem 'railties', github: 'rails/rails' + gem 'activerecord', github: 'rails/rails', branch: '4-2-stable' + gem 'activemodel', github: 'rails/rails', branch: '4-2-stable' + gem 'activesupport', github: 'rails/rails', branch: '4-2-stable' + gem 'actionpack', github: 'rails/rails', branch: '4-2-stable' + gem 'railties', github: 'rails/rails', branch: '4-2-stable' - gem 'arel', github: 'rails/arel' + gem 'arel', github: 'rails/arel', branch: '6-0-stable' gem 'journey', github: 'rails/journey' gem 'activerecord-deprecated_finders' diff --git a/History.md b/History.md index 21513c40a..3720ea6aa 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,129 @@ +## 1.6.2 / 2015-07-20 + +* Changes and bug fixes since 1.6.1 + + * Oracle enhanced adapter v1.6 requires ActiveRecord 4.2.1 or higher, + ActiveRecord 4.2.0 is not supported.[#672] + * Unique constraints not created when function unique index created [#662, #663] + * create_table should use default tablespace values for lobs [#668] + +## 1.6.1 / 2015-07-01 + +* Changes and bug fixes since 1.6.0 + + * Oracle enhanced adapter v1.6 requires ActiveRecord 4.2.1 or higher, + ActiveRecord 4.2.0 is not supported.[#651, #652] + * Fix serialized value becomes from yaml to string once saved [#655, #657] + * Update Ruby version in readme [#654] + * Update unit test matcher to skip sql statements to get `table` metadata [#653] + +## 1.6.0 / 2015-06-25 + +* Changes and bug fixes since 1.6.0.beta1 + + * Add deprecation warnings for Oracle enhanced specific foreign key methods [#631] + * Skip composite foreign key tests not supported in this version [#632] + * Do not dump default foreign key name [#633] + * Schema dump uses `:on_delete` option instead of `:dependent` [#634] + * Use Rails foreign key name in rspec unit tests [#635] + * Add deprecate warning if foreign key name length is longer than 30 byte [#636] + * Foreign key name longer than 30 byte will be shortened using Digest::SHA1.hexdigest [#637] + * Schema dumper for :integer will not dump :precision 0 [#638] + * Update foreign key names for add_foreign_key with table_name_prefix [#643] + +* Known Issues since 1.6.0.beta1 + * table_name_prefix and table_name_suffix changes column names which cause ORA-00904 [#639] + * custom methods should rollback record when exception is raised in after_create callback fails [#640] + * custom methods for create, update and destroy should log create record fails [#641] + +## 1.6.0.beta 1 / 2015-06-19 + +* Enhancements + * Support Rails 4.2 + * Support Rails native foreign key syntax [#488, #618] + +* Other changes and bug fixes + * Column#primary method removed from Rails [#483] + * ActiveRecord::Migrator.proper_table_name has been removed from Rails [#481] + * New db/schema.rb files will be created with force: :cascade [#593] + * Rails42 add unique index creates unique constraint [#617] + * Modify remove_column to add cascade constraint to avoid ORA-12991 [#617] + * Add `null: true` to avoid DEPRECATION WARNING [#489, #499] + * Rails 4.2 Add `connection.supports_views?` [#496] + * text? has been removed from Column class [#487] + * Remove call to deprecated `serialized_attributes` [#550, #552] + * Support :cascade option for drop_table [#579] + * Raise a better exception for renaming long indexes [#577] + * Override aliased_types [#575] + * Revert "Add options_include_default!" [#586] + * Remove substitute_at method from Oracle enhanced adapter [#520] + * Require 'active_record/base' in rake task #526 + * Make it easier to spot which version of active record is actually used [#550] + * Rails4.2 Add Type::Raw type [#503] + * Support :bigint datatype [#580] + * Map :bigint as NUMBER(19) sql_type not NUMBER(8) [#608] + * Use Oracle BINARY_FLOAT datatype for Rails :float type [#610] + * Revert "Implement possibility of handling of NUMBER columns as :float" [#576] + * Rails 4.2 Support NCHAR correctly [#490] + * Support :timestamp datatype in Rails 4.2 [#575] + * Rails 4.2 Handle NUMBER sql_type as `Type::Integer` cast type [#509] + * ActiveRecord::OracleEnhanced::Type::Integer for max_value to take 38 digits [#605] + * Rails 4.2 add `module OracleEnhanced` and migrate classes/modules under this [#584] + * Migrate to ActiveRecord::ConnectionAdapters::OracleEnhanced::ColumnDumper [#597] + * Migrated from OracleEnhancedContextIndex to OracleEnhanced::ContextIndex [#599] + * Make OracleEnhancedIndexDefinition as subclass of IndexDefinition [#600] + * Refactor add_index and add_index_options [#601] + * Types namespace moved to `ActiveRecord::Type::Value` [#484] + * Add new_column method [#482] + * Rename type_cast to type_cast_from_database [#485] + * Removed `forced_column_type` by using `cast_type` [#595] + * Move dump_schema_information to SchemaStatements [#611] + * Move OracleEnhancedIndexDefinition to OracleEnhanced::IndexDefinition [#614] + * Move OracleEnhancedSynonymDefinition to OracleEnhanced::SynonymDefinition [#615] + * Move types under OracleEnhanced module [#603] + * Make OracleEnhancedForeignKeyDefinition as subclass of ForeignKeyDefinition [#581] + * Support _field_changed argument changes [#479] + * Rails 4.2 Don't type cast the default on the column [#504] + * Rename variable names in create_table to follow Rails implementation [#616] + * Rails 4.2: Fix create_savepoint and rollback_to_savepoint name [#497] + * Shorten foreign key name if it is longer than 30 byte [#621] + * Restore foreign_key_definition [#624] + * Rails 4.2 Support OracleEnhancedAdapter.emulate_integers_by_column_name [#491] + * Rails 4.2 Support OracleEnhancedAdapter.emulate_dates_by_column_name [#492] + * Rails 4.2 Support emulate_booleans_from_strings and is_boolean_column? [#506] + * Rails 4.2 Support OracleEnhancedAdapter.number_datatype_coercion [#512] + * Rails 4.2 Use register_class_with_limit [#502] + * Rails 4.2 Remove redundant substitute index when constructing bind values [#517] + * Rails 4.2 Unit test updated to support `substitute_at` in Arel [#522] + * Change log method signiture to support Rails 4.2 [#539] + * Enable loading spec configuration from config file instead of env [#550] + * Rails42: Issue with non-existent columns [#545, #551] + * Squelch warning "#column_for_attribute` will return a null object + for non-existent columns in Rails 5. Use `#has_attribute?`" [#551] + * Use arel 6-0-stable [#565] + * Support 'Y' as true and 'N' as false in Rails 4.2 [#574, #573] + * Remove alias_method_chain :references, :foreign_keys [#582] + * Use quote_value method to avoid undefined method `type_cast_for_database' for nil:NilClass [#486] + * Rails 4.2: Set @nchar and @object_type only when sql_type is true [#493] + * Rails 4.2: Handle forced_column_type temporary [#498] + * Rails 4.2 Address ArgumentError: wrong number of arguments (1 for 2) at `quote_value` [#511] + * Address ORA-00932: inconsistent datatypes: expected NUMBER got DATE [#538] + * Remove duplicate alias_method_chain for indexes [#560] + * Address RangeError: 6000000000 is out of range for ActiveRecord::Type::Integer + with limit 4 [#578] + * Return foreign_keys_without_oracle_enhanced when non Oracle database used [#583] + * Add missing database_tasks.rb to gemspec [#585] + * Fixed typo in the rake tasks load statement [#587] + * Call super when column typs is serialized [#563, #591] + * Clear query cache on rollback [#592] + * Modify default to `false` if database default value is "N" [#596] + * refer correct location if filess in gemspec [#606] + * Add integer.rb to gemspec [#607] + +* Known Issues + * Override aliased_types [#575] + * Multi column foreign key is not supported + ## 1.5.6 / 2015-03-30 * Enhancements diff --git a/README.md b/README.md index c1d5a80a2..9c0a8640a 100644 --- a/README.md +++ b/README.md @@ -10,24 +10,40 @@ Oracle enhanced ActiveRecord adapter provides Oracle database access from Ruby o INSTALLATION ------------ +### Rails 4.2 -### Rails 4 +Oracle enhanced adapter version 1.6 just supports Rails 4.2 and does not support Rails 4.1 or lower version of Rails. +When using Ruby on Rails version 4.2 then in Gemfile include -Oracle enhanced adapter version 1.5 just supports Rails 4 and does not support Rails 3.2 or lower version of Rails. +```ruby +gem 'activerecord-oracle_enhanced-adapter', '~> 1.6.0' +``` -When using Ruby on Rails version 4 then in Gemfile include +where instead of 1.6.0 you can specify any other desired version. It is recommended to specify version with `~>` which means that use specified version or later patch versions (in this example any later 1.5.x version but not 1.6.x version). Oracle enhanced adapter maintains API backwards compatibility during patch version upgrades and therefore it is safe to always upgrade to latest patch version. - gem "activerecord-oracle_enhanced-adapter", "~> 1.5.0" +### Rails 4.0 and 4.1 + +Oracle enhanced adapter version 1.5 supports Rails 4.0 and 4.1 and does not support Rails 3.2 or lower version of Rails. + +When using Ruby on Rails version 4.0 and 4.1 then in Gemfile include + +```ruby +gem 'activerecord-oracle_enhanced-adapter', '~> 1.5.0' +``` where instead of 1.5.0 you can specify any other desired version. It is recommended to specify version with `~>` which means that use specified version or later patch versions (in this example any later 1.5.x version but not 1.6.x version). Oracle enhanced adapter maintains API backwards compatibility during patch version upgrades and therefore it is safe to always upgrade to latest patch version. If you would like to use latest adapter version from github then specify - gem 'activerecord-oracle_enhanced-adapter', :git => 'git://github.com/rsim/oracle-enhanced.git' +```ruby +gem 'activerecord-oracle_enhanced-adapter', :git => 'git://github.com/rsim/oracle-enhanced.git' +``` -If you are using CRuby 1.9.3 or 2.0 then you need to install ruby-oci8 gem as well as Oracle client, e.g. [Oracle Instant Client](http://www.oracle.com/technetwork/database/features/instant-client/index-097480.html). Include in Gemfile also ruby-oci8: +If you are using CRuby >= 1.9.3 then you need to install ruby-oci8 gem as well as Oracle client, e.g. [Oracle Instant Client](http://www.oracle.com/technetwork/database/features/instant-client/index-097480.html). Include in Gemfile also ruby-oci8: - gem 'ruby-oci8', '~> 2.1.0' +```ruby +gem 'ruby-oci8', '~> 2.1.0' +``` If you are using JRuby then you need to download latest [Oracle JDBC driver](http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html) - either ojdbc7.jar or ojdbc6.jar for Java 7, ojdbc6.jar for Java 6 or ojdbc5.jar for Java 5. And copy this file to one of these locations: @@ -38,7 +54,9 @@ If you are using JRuby then you need to download latest [Oracle JDBC driver](htt After specifying necessary gems in Gemfile run - bundle install +```bash +bundle install +``` to install the adapter (or later run `bundle update` to force updating to latest version). @@ -46,17 +64,23 @@ to install the adapter (or later run `bundle update` to force updating to latest When using Ruby on Rails version 3 then in Gemfile include - gem 'activerecord-oracle_enhanced-adapter', '~> 1.4.0' +```ruby +gem 'activerecord-oracle_enhanced-adapter', '~> 1.4.0' +``` where instead of 1.4.0 you can specify any other desired version. It is recommended to specify version with `~>` which means that use specified version or later patch versions (in this example any later 1.4.x version but not 1.5.x version). Oracle enhanced adapter maintains API backwards compatibility during patch version upgrades and therefore it is safe to always upgrade to latest patch version. If you would like to use latest adapter version from github then specify - gem 'activerecord-oracle_enhanced-adapter', :git => 'git://github.com/rsim/oracle-enhanced.git' +```ruby +gem 'activerecord-oracle_enhanced-adapter', :git => 'git://github.com/rsim/oracle-enhanced.git' +``` If you are using MRI 1.8 or 1.9 Ruby implementation then you need to install ruby-oci8 gem as well as Oracle client, e.g. [Oracle Instant Client](http://www.oracle.com/technetwork/database/features/instant-client/index-097480.html). Include in Gemfile also ruby-oci8: - gem 'ruby-oci8', '~> 2.1.0' +```ruby +gem 'ruby-oci8', '~> 2.1.0' +``` If you are using JRuby then you need to download latest [Oracle JDBC driver](http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html) - either ojdbc6.jar for Java 6 or ojdbc5.jar for Java 5. And copy this file to one of these locations: @@ -67,7 +91,9 @@ If you are using JRuby then you need to download latest [Oracle JDBC driver](htt After specifying necessary gems in Gemfile run - bundle install +```bash +bundle install +``` to install the adapter (or later run `bundle update` to force updating to latest version). @@ -75,12 +101,14 @@ to install the adapter (or later run `bundle update` to force updating to latest If you don't use Bundler in Rails 2 application then you need to specify gems in `config/environment.rb`, e.g. - Rails::Initializer.run do |config| - #... - config.gem 'activerecord-oracle_enhanced-adapter', :lib => "active_record/connection_adapters/oracle_enhanced_adapter" - config.gem 'ruby-oci8' - #... - end +```ruby +Rails::Initializer.run do |config| + # ... + config.gem 'activerecord-oracle_enhanced-adapter', :lib => 'active_record/connection_adapters/oracle_enhanced_adapter' + config.gem 'ruby-oci8' + # ... +end +``` But it is recommended to use Bundler for gem version management also for Rails 2.3 applications (search for instructions in Google). @@ -88,7 +116,9 @@ But it is recommended to use Bundler for gem version management also for Rails 2 If you want to use ActiveRecord and Oracle enhanced adapter without Rails and Bundler then install it just as a gem: - gem install activerecord-oracle_enhanced-adapter +```bash +gem install activerecord-oracle_enhanced-adapter +``` USAGE ----- @@ -97,52 +127,64 @@ USAGE In Rails application `config/database.yml` use oracle_enhanced as adapter name, e.g. - development: - adapter: oracle_enhanced - database: xe - username: user - password: secret +```yml +development: + adapter: oracle_enhanced + database: xe + username: user + password: secret +``` If you're connecting to a service name, indicate the service with a leading slash on the database parameter: - development: - adapter: oracle_enhanced - database: /xe - username: user - password: secret +```yml +development: + adapter: oracle_enhanced + database: /xe + username: user + password: secret +``` If `TNS_ADMIN` environment variable is pointing to directory where `tnsnames.ora` file is located then you can use TNS connection name in `database` parameter. Otherwise you can directly specify database host, port (defaults to 1521) and database name in the following way: - development: - adapter: oracle_enhanced - host: localhost - port: 1521 - database: xe - username: user - password: secret +```yml +development: + adapter: oracle_enhanced + host: localhost + port: 1521 + database: xe + username: user + password: secret +``` or you can use Oracle specific format in `database` parameter: - development: - adapter: oracle_enhanced - database: //localhost:1521/xe - username: user - password: secret +```yml +development: + adapter: oracle_enhanced + database: //localhost:1521/xe + username: user + password: secret +``` or you can even use Oracle specific TNS connection description: - development: - adapter: oracle_enhanced - database: "(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=tcp)(HOST=localhost)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=xe)))" - username: user - password: secret +```yml +development: + adapter: oracle_enhanced + database: "(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=tcp)(HOST=localhost)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=xe)))" + username: user + password: secret +``` If you deploy JRuby on Rails application in Java application server that supports JNDI connections then you can specify JNDI connection as well: - development: - adapter: oracle_enhanced - jndi: "jdbc/jndi_connection_name" +```yml +development: + adapter: oracle_enhanced + jndi: "jdbc/jndi_connection_name" +``` To use jndi with Tomcat you need to set the accessToUnderlyingConnectionAllowed to true property on the pool. See the [Tomcat Documentation](http://tomcat.apache.org/tomcat-7.0-doc/jndi-resources-howto.html) for reference. @@ -152,22 +194,28 @@ You can find other available database.yml connection parameters in [oracle_enhan If you want to change Oracle enhanced adapter default settings then create initializer file e.g. `config/initializers/oracle.rb` specify there necessary defaults, e.g.: - # It is recommended to set time zone in TZ environment variable so that the same timezone will be used by Ruby and by Oracle session - ENV['TZ'] = 'UTC' - - ActiveSupport.on_load(:active_record) do - ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do - # id columns and columns which end with _id will always be converted to integers - self.emulate_integers_by_column_name = true - # DATE columns which include "date" in name will be converted to Date, otherwise to Time - self.emulate_dates_by_column_name = true - # true and false will be stored as 'Y' and 'N' - self.emulate_booleans_from_strings = true - # start primary key sequences from 1 (and not 10000) and take just one next value in each session - self.default_sequence_start_value = "1 NOCACHE INCREMENT BY 1" - # other settings ... - end - end +```ruby +# It is recommended to set time zone in TZ environment variable so that the same timezone will be used by Ruby and by Oracle session +ENV['TZ'] = 'UTC' + +ActiveSupport.on_load(:active_record) do + ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do + # id columns and columns which end with _id will always be converted to integers + self.emulate_integers_by_column_name = true + + # DATE columns which include "date" in name will be converted to Date, otherwise to Time + self.emulate_dates_by_column_name = true + + # true and false will be stored as 'Y' and 'N' + self.emulate_booleans_from_strings = true + + # start primary key sequences from 1 (and not 10000) and take just one next value in each session + self.default_sequence_start_value = "1 NOCACHE INCREMENT BY 1" + + # other settings ... + end +end +``` In case of Rails 2 application you do not need to use `ActiveSupport.on_load(:active_record) do ... end` around settings code block. @@ -177,81 +225,108 @@ See other adapter settings in [oracle_enhanced_adapter.rb](http://github.com/rsi If you want to put Oracle enhanced adapter on top of existing schema tables then there are several methods how to override ActiveRecord defaults, see example: - class Employee < ActiveRecord::Base - # specify schema and table name - self.table_name = "hr.hr_employees" - # specify primary key name - self.primary_key = "employee_id" - # specify sequence name - self.sequence_name = "hr.hr_employee_s" - # set which DATE columns should be converted to Ruby Date - set_date_columns :hired_on, :birth_date_on - # set which DATE columns should be converted to Ruby Time - set_datetime_columns :last_login_time - # set which VARCHAR2 columns should be converted to true and false - set_boolean_columns :manager, :active - # set which columns should be ignored in ActiveRecord - ignore_table_columns :attribute1, :attribute2 - end +```ruby +class Employee < ActiveRecord::Base + # specify schema and table name + self.table_name = "hr.hr_employees" + + # specify primary key name + self.primary_key = "employee_id" + + # specify sequence name + self.sequence_name = "hr.hr_employee_s" + + # set which DATE columns should be converted to Ruby Date + set_date_columns :hired_on, :birth_date_on + + # set which DATE columns should be converted to Ruby Time + set_datetime_columns :last_login_time + + # set which VARCHAR2 columns should be converted to true and false + set_boolean_columns :manager, :active + + # set which columns should be ignored in ActiveRecord + ignore_table_columns :attribute1, :attribute2 +end +``` You can also access remote tables over database link using - self.table_name "hr_employees@db_link" +```ruby +self.table_name "hr_employees@db_link" +``` Examples for Rails 3.2 and lower version of Rails - class Employee < ActiveRecord::Base - # specify schema and table name - set_table_name "hr.hr_employees" - # specify primary key name - set_primary_key "employee_id" - # specify sequence name - set_sequence_name "hr.hr_employee_s" - # set which DATE columns should be converted to Ruby Date - set_date_columns :hired_on, :birth_date_on - # set which DATE columns should be converted to Ruby Time - set_datetime_columns :last_login_time - # set which VARCHAR2 columns should be converted to true and false - set_boolean_columns :manager, :active - # set which columns should be ignored in ActiveRecord - ignore_table_columns :attribute1, :attribute2 - end +```ruby +class Employee < ActiveRecord::Base + # specify schema and table name + set_table_name "hr.hr_employees" + + # specify primary key name + set_primary_key "employee_id" + + # specify sequence name + set_sequence_name "hr.hr_employee_s" + + # set which DATE columns should be converted to Ruby Date + set_date_columns :hired_on, :birth_date_on + + # set which DATE columns should be converted to Ruby Time + set_datetime_columns :last_login_time + + # set which VARCHAR2 columns should be converted to true and false + set_boolean_columns :manager, :active + + # set which columns should be ignored in ActiveRecord + ignore_table_columns :attribute1, :attribute2 +end +``` You can also access remote tables over database link using - set_table_name "hr_employees@db_link" +```ruby +set_table_name "hr_employees@db_link" +``` ### Custom create, update and delete methods If you have legacy schema and you are not allowed to do direct INSERTs, UPDATEs and DELETEs in legacy schema tables and need to use existing PL/SQL procedures for create, updated, delete operations then you should add `ruby-plsql` gem to your application, include `ActiveRecord::OracleEnhancedProcedures` in your model and then define custom create, update and delete methods, see example: - class Employee < ActiveRecord::Base - include ActiveRecord::OracleEnhancedProcedures - # when defining create method then return ID of new record that will be assigned to id attribute of new object - set_create_method do - plsql.employees_pkg.create_employee( - :p_first_name => first_name, - :p_last_name => last_name, - :p_employee_id => nil - )[:p_employee_id] - end - set_update_method do - plsql.employees_pkg.update_employee( - :p_employee_id => id, - :p_first_name => first_name, - :p_last_name => last_name - ) - end - set_delete_method do - plsql.employees_pkg.delete_employee( - :p_employee_id => id - ) - end - end +```ruby +class Employee < ActiveRecord::Base + include ActiveRecord::OracleEnhancedProcedures + + # when defining create method then return ID of new record that will be assigned to id attribute of new object + set_create_method do + plsql.employees_pkg.create_employee( + :p_first_name => first_name, + :p_last_name => last_name, + :p_employee_id => nil + )[:p_employee_id] + end + + set_update_method do + plsql.employees_pkg.update_employee( + :p_employee_id => id, + :p_first_name => first_name, + :p_last_name => last_name + ) + end + + set_delete_method do + plsql.employees_pkg.delete_employee( + :p_employee_id => id + ) + end +end +``` In addition in `config/initializers/oracle.rb` initializer specify that ruby-plsql should use ActiveRecord database connection: - plsql.activerecord_class = ActiveRecord::Base +```ruby +plsql.activerecord_class = ActiveRecord::Base +``` ### Oracle CONTEXT index support @@ -259,60 +334,78 @@ Every edition of Oracle database includes [Oracle Text](http://www.oracle.com/te To create simple single column index create migration with, e.g. - add_context_index :posts, :title +```ruby +add_context_index :posts, :title +``` and you can remove context index with - remove_context_index :posts, :title +```ruby +remove_context_index :posts, :title +``` Include in class definition - has_context_index +```ruby +has_context_index +``` and then you can do full text search with - Post.contains(:title, 'word') +```ruby +Post.contains(:title, 'word') +``` You can create index on several columns (which will generate additional stored procedure for providing XML document with specified columns to indexer): - add_context_index :posts, [:title, :body] +```ruby +add_context_index :posts, [:title, :body] +``` And you can search either in all columns or specify in which column you want to search (as first argument you need to specify first column name as this is the column which is referenced during index creation): - Post.contains(:title, 'word') - Post.contains(:title, 'word within title') - Post.contains(:title, 'word within body') +```ruby +Post.contains(:title, 'word') +Post.contains(:title, 'word within title') +Post.contains(:title, 'word within body') +``` See Oracle Text documentation for syntax that you can use in CONTAINS function in SELECT WHERE clause. You can also specify some dummy main column name when creating multiple column index as well as specify to update index automatically after each commit (as otherwise you need to synchronize index manually or schedule periodic update): - add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT' +```ruby +add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT' - Post.contains(:all_text, 'word') +Post.contains(:all_text, 'word') +``` Or you can specify that index should be updated when specified columns are updated (e.g. in ActiveRecord you can specify to trigger index update when created_at or updated_at columns are updated). Otherwise index is updated only when main index column is updated. - add_context_index :posts, [:title, :body], :index_column => :all_text, - :sync => 'ON COMMIT', :index_column_trigger_on => [:created_at, :updated_at] +```ruby +add_context_index :posts, [:title, :body], :index_column => :all_text, + :sync => 'ON COMMIT', :index_column_trigger_on => [:created_at, :updated_at] +``` And you can even create index on multiple tables by providing SELECT statements which should be used to fetch necessary columns from related tables: - add_context_index :posts, - [:title, :body, - # specify aliases always with AS keyword - "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id" - ], - :name => 'post_and_comments_index', - :index_column => :all_text, - :index_column_trigger_on => [:updated_at, :comments_count], - :sync => 'ON COMMIT' - - # search in any table columns - Post.contains(:all_text, 'word') - # search in specified column - Post.contains(:all_text, "aaa within title") - Post.contains(:all_text, "bbb within comment_author") +```ruby +add_context_index :posts, + [:title, :body, + # specify aliases always with AS keyword + "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id" + ], + :name => 'post_and_comments_index', + :index_column => :all_text, + :index_column_trigger_on => [:updated_at, :comments_count], + :sync => 'ON COMMIT' + +# search in any table columns +Post.contains(:all_text, 'word') +# search in specified column +Post.contains(:all_text, "aaa within title") +Post.contains(:all_text, "bbb within comment_author") +``` ### Oracle virtual columns support @@ -321,43 +414,53 @@ They can be used as normal fields in the queries, in the foreign key contstraint To define virtual column you can use `virtual` method in the `create_table` block, providing column expression in the `:as` option: - create_table :mytable do |t| - t.decimal :price, :precision => 15, :scale => 2 - t.decimal :quantity, :precision => 15, :scale => 2 - t.virtual :amount, :as => 'price * quantity' - end +```ruby +create_table :mytable do |t| + t.decimal :price, :precision => 15, :scale => 2 + t.decimal :quantity, :precision => 15, :scale => 2 + t.virtual :amount, :as => 'price * quantity' +end +``` Oracle tries to predict type of the virtual column, based on its expression but sometimes it is necessary to state type explicitly. This can be done by providing `:type` option to the `virtual` method: - ... - t.virtual :amount_2, :as => 'ROUND(price * quantity,2)', :type => :decimal, :precision => 15, :scale => 2 - t.virtual :amount_str, :as => "TO_CHAR(quantity) || ' x ' || TO_CHAR(price) || ' USD = ' || TO_CHAR(quantity*price) || ' USD'", - :type => :string, :limit => 100 - ... +```ruby +# ... +t.virtual :amount_2, :as => 'ROUND(price * quantity,2)', :type => :decimal, :precision => 15, :scale => 2 +t.virtual :amount_str, :as => "TO_CHAR(quantity) || ' x ' || TO_CHAR(price) || ' USD = ' || TO_CHAR(quantity*price) || ' USD'", + :type => :string, :limit => 100 +# ... +``` It is possible to add virtual column to existing table: - add_column :mytable, :amount_4, :virtual, :as => 'ROUND(price * quantity,4)', :precision => 38, :scale => 4 +```ruby +add_column :mytable, :amount_4, :virtual, :as => 'ROUND(price * quantity,4)', :precision => 38, :scale => 4 +``` You can use the same options here as in the `create_table` `virtual` method. Changing virtual columns is also possible: - change_column :mytable, :amount, :virtual, :as => 'ROUND(price * quantity,0)', :type => :integer +```ruby +change_column :mytable, :amount, :virtual, :as => 'ROUND(price * quantity,0)', :type => :integer +``` Virtual columns allowed in the foreign key constraints. For example it can be used to force foreign key constraint on polymorphic association: - create_table :comments do |t| - t.string :subject_type - t.integer :subject_id - t.virtual :subject_photo_id, :as => "CASE subject_type WHEN 'Photo' THEN subject_id END" - t.virtual :subject_event_id, :as => "CASE subject_type WHEN 'Event' THEN subject_id END" - end +```ruby +create_table :comments do |t| + t.string :subject_type + t.integer :subject_id + t.virtual :subject_photo_id, :as => "CASE subject_type WHEN 'Photo' THEN subject_id END" + t.virtual :subject_event_id, :as => "CASE subject_type WHEN 'Event' THEN subject_id END" +end - add_foreign_key :comments, :photos, :column => :subject_photo_id - add_foreign_key :comments, :events, :column => :subject_event_id +add_foreign_key :comments, :photos, :column => :subject_photo_id +add_foreign_key :comments, :events, :column => :subject_event_id +``` For backward compatibility reasons it is possible to use `:default` option in the `create_table` instead of `:as` option. But this is deprecated and may be removed in the future version. @@ -373,8 +476,10 @@ There are several additional schema statements and data types available that you * You can add table and column comments with `:comment` option * Default tablespaces can be specified for tables, indexes, clobs and blobs, for example: - ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces = - {:clob => 'TS_LOB', :blob => 'TS_LOB', :index => 'TS_INDEX', :table => 'TS_DATA'} +```ruby +ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces = + {:clob => 'TS_LOB', :blob => 'TS_LOB', :index => 'TS_INDEX', :table => 'TS_DATA'} +``` TROUBLESHOOTING --------------- @@ -395,13 +500,15 @@ Please verify that 3. Verify that activerecord-oracle_enhanced-adapter is working from irb - require 'rubygems' - gem 'activerecord' - gem 'activerecord-oracle_enhanced-adapter' - require 'active_record' - ActiveRecord::Base.establish_connection(:adapter => "oracle_enhanced", :database => "database",:username => "user",:password => "password") +```ruby +require 'rubygems' +gem 'activerecord' +gem 'activerecord-oracle_enhanced-adapter' +require 'active_record' +ActiveRecord::Base.establish_connection(:adapter => "oracle_enhanced", :database => "database",:username => "user",:password => "password") +``` - and see if it is successful (use your correct database, username and password) +and see if it is successful (use your correct database, username and password) ### What to do if Oracle enhanced adapter is not working with Phusion Passenger? diff --git a/Rakefile b/Rakefile index 1f28bbbcc..8b2ccd24c 100644 --- a/Rakefile +++ b/Rakefile @@ -12,15 +12,15 @@ require 'rake' require 'jeweler' Jeweler::Tasks.new do |gem| - gem.name = "activerecord-oracle_enhanced-adapter" + gem.name = "pmacs-activerecord-oracle_enhanced-adapter" gem.summary = "Oracle enhanced adapter for ActiveRecord" gem.description = <<-EOS Oracle "enhanced" ActiveRecord adapter contains useful additional methods for working with new and legacy Oracle databases. This adapter is superset of original ActiveRecord Oracle adapter. EOS - gem.email = "raimonds.simanovskis@gmail.com" - gem.homepage = "http://github.com/rsim/oracle-enhanced" - gem.authors = ["Raimonds Simanovskis"] + gem.email = "charles.treatman@gmail.com" + gem.homepage = "http://github.com/pmacs/oracle-enhanced" + gem.authors = ["Charles Treatman", "Raimonds Simanovskis"] gem.extra_rdoc_files = ['README.md'] gem.license = 'MIT' end @@ -54,7 +54,7 @@ Rake::RDocTask.new do |rdoc| version = File.exist?('VERSION') ? File.read('VERSION') : "" rdoc.rdoc_dir = 'doc' - rdoc.title = "activerecord-oracle_enhanced-adapter #{version}" + rdoc.title = "pmacs-activerecord-oracle_enhanced-adapter #{version}" rdoc.rdoc_files.include('README*') rdoc.rdoc_files.include('lib/**/*.rb') end diff --git a/VERSION b/VERSION index eac1e0ada..47b998336 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.6 +1.6.2.1 diff --git a/lib/active_record/connection_adapters/oracle_enhanced_column.rb b/lib/active_record/connection_adapters/oracle_enhanced/column.rb similarity index 60% rename from lib/active_record/connection_adapters/oracle_enhanced_column.rb rename to lib/active_record/connection_adapters/oracle_enhanced/column.rb index 8a47823a3..b8131df4c 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_column.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/column.rb @@ -2,11 +2,13 @@ module ActiveRecord module ConnectionAdapters #:nodoc: class OracleEnhancedColumn < Column - attr_reader :table_name, :forced_column_type, :nchar, :virtual_column_data_default, :returning_id #:nodoc: + attr_reader :table_name, :nchar, :virtual_column_data_default, :returning_id #:nodoc: - def initialize(name, default, sql_type = nil, null = true, table_name = nil, forced_column_type = nil, virtual=false, returning_id=false) #:nodoc: + FALSE_VALUES << 'N' + TRUE_VALUES << 'Y' + + def initialize(name, default, cast_type, sql_type = nil, null = true, table_name = nil, virtual=false, returning_id=false) #:nodoc: @table_name = table_name - @forced_column_type = forced_column_type @virtual = virtual @virtual_column_data_default = default.inspect if virtual @returning_id = returning_id @@ -15,32 +17,20 @@ def initialize(name, default, sql_type = nil, null = true, table_name = nil, for else default_value = self.class.extract_value_from_default(default) end - super(name, default_value, sql_type, null) + super(name, default_value, cast_type, sql_type, null) # Is column NCHAR or NVARCHAR2 (will need to use N'...' value quoting for these data types)? # Define only when needed as adapter "quote" method will check at first if instance variable is defined. - @nchar = true if @type == :string && sql_type[0,1] == 'N' - @object_type = sql_type.include? '.' - end - - def type_cast(value) #:nodoc: - case type - when :raw - OracleEnhancedColumn.string_to_raw(value) - when :datetime - OracleEnhancedAdapter.emulate_dates ? guess_date_or_time(value) : super - when :float - !value.nil? ? self.class.value_to_decimal(value) : super - else - super + if sql_type + @nchar = true if cast_type.class == ActiveRecord::Type::String && sql_type[0,1] == 'N' + @object_type = sql_type.include? '.' end + # TODO: Need to investigate when `sql_type` becomes nil end - def type_cast_code(var_name) - type == :float ? "#{self.class.name}.value_to_decimal(#{var_name})" : super - end - - def klass - type == :float ? BigDecimal : super + def type_cast(value) #:nodoc: + return OracleEnhancedColumn::string_to_raw(value) if type == :raw + return guess_date_or_time(value) if type == :datetime && OracleEnhancedAdapter.emulate_dates + super end def virtual? @@ -96,45 +86,6 @@ def comment private - def simplified_type(field_type) - forced_column_type || - case field_type - when /decimal|numeric|number/i - if OracleEnhancedAdapter.emulate_booleans && field_type.upcase == "NUMBER(1)" - :boolean - elsif extract_scale(field_type) == 0 || - # if column name is ID or ends with _ID - OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(name, table_name) - :integer - elsif field_type.upcase == "NUMBER" - OracleEnhancedAdapter.number_datatype_coercion - else - :decimal - end - when /raw/i - :raw - when /char/i - if OracleEnhancedAdapter.emulate_booleans_from_strings && - OracleEnhancedAdapter.is_boolean_column?(name, field_type, table_name) - :boolean - else - :string - end - when /date/i - if OracleEnhancedAdapter.emulate_dates_by_column_name && OracleEnhancedAdapter.is_date_column?(name, table_name) - :date - else - :datetime - end - when /timestamp/i - :timestamp - when /time/i - :datetime - else - super - end - end - def self.extract_value_from_default(default) case default when String diff --git a/lib/active_record/connection_adapters/oracle_enhanced/column_dumper.rb b/lib/active_record/connection_adapters/oracle_enhanced/column_dumper.rb new file mode 100644 index 000000000..4448bd847 --- /dev/null +++ b/lib/active_record/connection_adapters/oracle_enhanced/column_dumper.rb @@ -0,0 +1,65 @@ +module ActiveRecord #:nodoc: + module ConnectionAdapters #:nodoc: + module OracleEnhanced #:nodoc: + module ColumnDumper #:nodoc: + def self.included(base) #:nodoc: + base.class_eval do + private + alias_method_chain :column_spec, :oracle_enhanced + alias_method_chain :prepare_column_options, :oracle_enhanced + alias_method_chain :migration_keys, :oracle_enhanced + + def oracle_enhanced_adapter? + # return original method if not using 'OracleEnhanced' + if (rails_env = defined?(Rails.env) ? Rails.env : (defined?(RAILS_ENV) ? RAILS_ENV : nil)) && + ActiveRecord::Base.configurations[rails_env] && + ActiveRecord::Base.configurations[rails_env]['adapter'] != 'oracle_enhanced' + return false + else + return true + end + end + end + end + + def column_spec_with_oracle_enhanced(column, types) + # return original method if not using 'OracleEnhanced' + return column_spec_without_oracle_enhanced(column, types) unless oracle_enhanced_adapter? + + spec = prepare_column_options(column, types) + (spec.keys - [:name, :type]).each do |k| + key_s = (k == :virtual_type ? "type: " : "#{k.to_s}: ") + spec[k] = key_s + spec[k] + end + spec + end + + def prepare_column_options_with_oracle_enhanced(column, types) + # return original method if not using 'OracleEnhanced' + return prepare_column_options_without_oracle_enhanced(column, types) unless oracle_enhanced_adapter? + + spec = {} + spec[:name] = column.name.inspect + spec[:type] = column.virtual? ? 'virtual' : column.type.to_s + spec[:virtual_type] = column.type.inspect if column.virtual? && column.sql_type != 'NUMBER' + spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && column.type != :decimal + spec[:precision] = column.precision.inspect if !column.precision.nil? + spec[:scale] = column.scale.inspect if !column.scale.nil? + spec[:null] = 'false' if !column.null + spec[:as] = column.virtual_column_data_default if column.virtual? + spec[:default] = schema_default(column) if column.has_default? && !column.virtual? + spec.delete(:default) if spec[:default].nil? + spec + end + + def migration_keys_with_oracle_enhanced + # TODO `& column_specs.map(&:keys).flatten` should be exetuted here + # return original method if not using 'OracleEnhanced' + return migration_keys_without_oracle_enhanced unless oracle_enhanced_adapter? + + [:name, :limit, :precision, :scale, :default, :null, :as, :virtual_type] + end + end + end + end +end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_connection.rb b/lib/active_record/connection_adapters/oracle_enhanced/connection.rb similarity index 97% rename from lib/active_record/connection_adapters/oracle_enhanced_connection.rb rename to lib/active_record/connection_adapters/oracle_enhanced/connection.rb index df8ef8b82..802eeca69 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/connection.rb @@ -109,11 +109,11 @@ class OracleEnhancedConnectionException < StandardError #:nodoc: # if MRI or YARV if !defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby' ORACLE_ENHANCED_CONNECTION = :oci - require 'active_record/connection_adapters/oracle_enhanced_oci_connection' + require 'active_record/connection_adapters/oracle_enhanced/oci_connection' # if JRuby elsif RUBY_ENGINE == 'jruby' ORACLE_ENHANCED_CONNECTION = :jdbc - require 'active_record/connection_adapters/oracle_enhanced_jdbc_connection' + require 'active_record/connection_adapters/oracle_enhanced/jdbc_connection' else raise "Unsupported Ruby engine #{RUBY_ENGINE}" end diff --git a/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb b/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb new file mode 100644 index 000000000..0e406ae0b --- /dev/null +++ b/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb @@ -0,0 +1,347 @@ +module ActiveRecord + module ConnectionAdapters + module OracleEnhanced + module ContextIndex + + # Define full text index with Oracle specific CONTEXT index type + # + # Oracle CONTEXT index by default supports full text indexing of one column. + # This method allows full text index creation also on several columns + # as well as indexing related table columns by generating stored procedure + # that concatenates all columns for indexing as well as generating trigger + # that will update main index column to trigger reindexing of record. + # + # Use +contains+ ActiveRecord model instance method to add CONTAINS where condition + # and order by score of matched results. + # + # Options: + # + # * :name + # * :index_column + # * :index_column_trigger_on + # * :tablespace + # * :sync - 'MANUAL', 'EVERY "interval-string"' or 'ON COMMIT' (defaults to 'MANUAL'). + # * :lexer - Lexer options (e.g. :type => 'BASIC_LEXER', :base_letter => true). + # * :wordlist - Wordlist options (e.g. :type => 'BASIC_WORDLIST', :prefix_index => true). + # * :transactional - When +true+, the CONTAINS operator will process inserted and updated rows. + # + # ===== Examples + # + # ====== Creating single column index + # add_context_index :posts, :title + # search with + # Post.contains(:title, 'word') + # + # ====== Creating index on several columns + # add_context_index :posts, [:title, :body] + # search with (use first column as argument for contains method but it will search in all index columns) + # Post.contains(:title, 'word') + # + # ====== Creating index on several columns with dummy index column and commit option + # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT' + # search with + # Post.contains(:all_text, 'word') + # + # ====== Creating index with trigger option (will reindex when specified columns are updated) + # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT', + # :index_column_trigger_on => [:created_at, :updated_at] + # search with + # Post.contains(:all_text, 'word') + # + # ====== Creating index on multiple tables + # add_context_index :posts, + # [:title, :body, + # # specify aliases always with AS keyword + # "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id" + # ], + # :name => 'post_and_comments_index', + # :index_column => :all_text, :index_column_trigger_on => [:updated_at, :comments_count], + # :sync => 'ON COMMIT' + # search in any table columns + # Post.contains(:all_text, 'word') + # search in specified column + # Post.contains(:all_text, "aaa within title") + # Post.contains(:all_text, "bbb within comment_author") + # + # ====== Creating index using lexer + # add_context_index :posts, :title, :lexer => { :type => 'BASIC_LEXER', :base_letter => true, ... } + # + # ====== Creating index using wordlist + # add_context_index :posts, :title, :wordlist => { :type => 'BASIC_WORDLIST', :prefix_index => true, ... } + # + # ====== Creating transactional index (will reindex changed rows when querying) + # add_context_index :posts, :title, :transactional => true + # + def add_context_index(table_name, column_name, options = {}) + self.all_schema_indexes = nil + column_names = Array(column_name) + index_name = options[:name] || index_name(table_name, :column => options[:index_column] || column_names, + # CONEXT index name max length is 25 + :identifier_max_length => 25) + + quoted_column_name = quote_column_name(options[:index_column] || column_names.first) + if options[:index_column_trigger_on] + raise ArgumentError, "Option :index_column should be specified together with :index_column_trigger_on option" \ + unless options[:index_column] + create_index_column_trigger(table_name, index_name, options[:index_column], options[:index_column_trigger_on]) + end + + sql = "CREATE INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" + sql << " (#{quoted_column_name})" + sql << " INDEXTYPE IS CTXSYS.CONTEXT" + parameters = [] + if column_names.size > 1 + procedure_name = default_datastore_procedure(index_name) + datastore_name = default_datastore_name(index_name) + create_datastore_procedure(table_name, procedure_name, column_names, options) + create_datastore_preference(datastore_name, procedure_name) + parameters << "DATASTORE #{datastore_name} SECTION GROUP CTXSYS.AUTO_SECTION_GROUP" + end + if options[:tablespace] + storage_name = default_storage_name(index_name) + create_storage_preference(storage_name, options[:tablespace]) + parameters << "STORAGE #{storage_name}" + end + if options[:sync] + parameters << "SYNC(#{options[:sync]})" + end + if options[:lexer] && (lexer_type = options[:lexer][:type]) + lexer_name = default_lexer_name(index_name) + (lexer_options = options[:lexer].dup).delete(:type) + create_lexer_preference(lexer_name, lexer_type, lexer_options) + parameters << "LEXER #{lexer_name}" + end + if options[:wordlist] && (wordlist_type = options[:wordlist][:type]) + wordlist_name = default_wordlist_name(index_name) + (wordlist_options = options[:wordlist].dup).delete(:type) + create_wordlist_preference(wordlist_name, wordlist_type, wordlist_options) + parameters << "WORDLIST #{wordlist_name}" + end + if options[:transactional] + parameters << "TRANSACTIONAL" + end + unless parameters.empty? + sql << " PARAMETERS ('#{parameters.join(' ')}')" + end + execute sql + end + + # Drop full text index with Oracle specific CONTEXT index type + def remove_context_index(table_name, options = {}) + self.all_schema_indexes = nil + unless Hash === options # if column names passed as argument + options = {:column => Array(options)} + end + index_name = options[:name] || index_name(table_name, + :column => options[:index_column] || options[:column], :identifier_max_length => 25) + execute "DROP INDEX #{index_name}" + drop_ctx_preference(default_datastore_name(index_name)) + drop_ctx_preference(default_storage_name(index_name)) + procedure_name = default_datastore_procedure(index_name) + execute "DROP PROCEDURE #{quote_table_name(procedure_name)}" rescue nil + drop_index_column_trigger(index_name) + end + + private + + def create_datastore_procedure(table_name, procedure_name, column_names, options) + quoted_table_name = quote_table_name(table_name) + select_queries, column_names = column_names.partition { |c| c.to_s =~ /^\s*SELECT\s+/i } + select_queries = select_queries.map { |s| s.strip.gsub(/\s+/, ' ') } + keys, selected_columns = parse_select_queries(select_queries) + quoted_column_names = (column_names+keys).map{|col| quote_column_name(col)} + execute compress_lines(<<-SQL) + CREATE OR REPLACE PROCEDURE #{quote_table_name(procedure_name)} + (p_rowid IN ROWID, + p_clob IN OUT NOCOPY CLOB) IS + -- add_context_index_parameters #{(column_names+select_queries).inspect}#{!options.empty? ? ', ' << options.inspect[1..-2] : ''} + #{ + selected_columns.map do |cols| + cols.map do |col| + raise ArgumentError, "Alias #{col} too large, should be 28 or less characters long" unless col.length <= 28 + "l_#{col} VARCHAR2(32767);\n" + end.join + end.join + } BEGIN + FOR r1 IN ( + SELECT #{quoted_column_names.join(', ')} + FROM #{quoted_table_name} + WHERE #{quoted_table_name}.ROWID = p_rowid + ) LOOP + #{ + (column_names.map do |col| + col = col.to_s + "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" << + "IF LENGTH(r1.#{col}) > 0 THEN\n" << + "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(r1.#{col}), r1.#{col});\n" << + "END IF;\n" << + "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '');\n" + end.join) << + (selected_columns.zip(select_queries).map do |cols, query| + (cols.map do |col| + "l_#{col} := '';\n" + end.join) << + "FOR r2 IN (\n" << + query.gsub(/:(\w+)/,"r1.\\1") << "\n) LOOP\n" << + (cols.map do |col| + "l_#{col} := l_#{col} || r2.#{col} || CHR(10);\n" + end.join) << + "END LOOP;\n" << + (cols.map do |col| + col = col.to_s + "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" << + "IF LENGTH(l_#{col}) > 0 THEN\n" << + "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(l_#{col}), l_#{col});\n" << + "END IF;\n" << + "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '');\n" + end.join) + end.join) + } + END LOOP; + END; + SQL + end + + def parse_select_queries(select_queries) + keys = [] + selected_columns = [] + select_queries.each do |query| + # get primary or foreign keys like :id or :something_id + keys << (query.scan(/:\w+/).map{|k| k[1..-1].downcase.to_sym}) + select_part = query.scan(/^select\s.*\sfrom/i).first + selected_columns << select_part.scan(/\sas\s+(\w+)/i).map{|c| c.first} + end + [keys.flatten.uniq, selected_columns] + end + + def create_datastore_preference(datastore_name, procedure_name) + drop_ctx_preference(datastore_name) + execute <<-SQL + BEGIN + CTX_DDL.CREATE_PREFERENCE('#{datastore_name}', 'USER_DATASTORE'); + CTX_DDL.SET_ATTRIBUTE('#{datastore_name}', 'PROCEDURE', '#{procedure_name}'); + END; + SQL + end + + def create_storage_preference(storage_name, tablespace) + drop_ctx_preference(storage_name) + sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{storage_name}', 'BASIC_STORAGE');\n" + ['I_TABLE_CLAUSE', 'K_TABLE_CLAUSE', 'R_TABLE_CLAUSE', + 'N_TABLE_CLAUSE', 'I_INDEX_CLAUSE', 'P_TABLE_CLAUSE'].each do |clause| + default_clause = case clause + when 'R_TABLE_CLAUSE'; 'LOB(DATA) STORE AS (CACHE) ' + when 'I_INDEX_CLAUSE'; 'COMPRESS 2 ' + else '' + end + sql << "CTX_DDL.SET_ATTRIBUTE('#{storage_name}', '#{clause}', '#{default_clause}TABLESPACE #{tablespace}');\n" + end + sql << "END;\n" + execute sql + end + + def create_lexer_preference(lexer_name, lexer_type, options) + drop_ctx_preference(lexer_name) + sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{lexer_name}', '#{lexer_type}');\n" + options.each do |key, value| + plsql_value = case value + when String; "'#{value}'" + when true; "'YES'" + when false; "'NO'" + when nil; 'NULL' + else value + end + sql << "CTX_DDL.SET_ATTRIBUTE('#{lexer_name}', '#{key}', #{plsql_value});\n" + end + sql << "END;\n" + execute sql + end + + def create_wordlist_preference(wordlist_name, wordlist_type, options) + drop_ctx_preference(wordlist_name) + sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{wordlist_name}', '#{wordlist_type}');\n" + options.each do |key, value| + plsql_value = case value + when String; "'#{value}'" + when true; "'YES'" + when false; "'NO'" + when nil; 'NULL' + else value + end + sql << "CTX_DDL.SET_ATTRIBUTE('#{wordlist_name}', '#{key}', #{plsql_value});\n" + end + sql << "END;\n" + execute sql + end + + def drop_ctx_preference(preference_name) + execute "BEGIN CTX_DDL.DROP_PREFERENCE('#{preference_name}'); END;" rescue nil + end + + def create_index_column_trigger(table_name, index_name, index_column, index_column_source) + trigger_name = default_index_column_trigger_name(index_name) + columns = Array(index_column_source) + quoted_column_names = columns.map{|col| quote_column_name(col)}.join(', ') + execute compress_lines(<<-SQL) + CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)} + BEFORE UPDATE OF #{quoted_column_names} ON #{quote_table_name(table_name)} FOR EACH ROW + BEGIN + :new.#{quote_column_name(index_column)} := '1'; + END; + SQL + end + + def drop_index_column_trigger(index_name) + trigger_name = default_index_column_trigger_name(index_name) + execute "DROP TRIGGER #{quote_table_name(trigger_name)}" rescue nil + end + + def default_datastore_procedure(index_name) + "#{index_name}_prc" + end + + def default_datastore_name(index_name) + "#{index_name}_dst" + end + + def default_storage_name(index_name) + "#{index_name}_sto" + end + + def default_index_column_trigger_name(index_name) + "#{index_name}_trg" + end + + def default_lexer_name(index_name) + "#{index_name}_lex" + end + + def default_wordlist_name(index_name) + "#{index_name}_wl" + end + + module BaseClassMethods + # Declare that model table has context index defined. + # As a result contains class scope method is defined. + def has_context_index + extend ContextIndexClassMethods + end + end + + module ContextIndexClassMethods + # Add context index condition. + def contains(column, query, options ={}) + score_label = options[:label].to_i || 1 + where("CONTAINS(#{connection.quote_column_name(column)}, ?, #{score_label}) > 0", query). + order("SCORE(#{score_label}) DESC") + end + end + + end + end + end +end + +ActiveRecord::Base.class_eval do + extend ActiveRecord::ConnectionAdapters::OracleEnhanced::ContextIndex::BaseClassMethods +end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb b/lib/active_record/connection_adapters/oracle_enhanced/cpk.rb similarity index 100% rename from lib/active_record/connection_adapters/oracle_enhanced_cpk.rb rename to lib/active_record/connection_adapters/oracle_enhanced/cpk.rb diff --git a/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb b/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb new file mode 100644 index 000000000..ba91a7633 --- /dev/null +++ b/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb @@ -0,0 +1,257 @@ +module ActiveRecord + module ConnectionAdapters + module OracleEnhanced + module DatabaseStatements + # DATABASE STATEMENTS ====================================== + # + # see: abstract/database_statements.rb + + # Executes a SQL statement + def execute(sql, name = nil) + log(sql, name) { @connection.exec(sql) } + end + + def clear_cache! + @statements.clear + reload_type_map + end + + def exec_query(sql, name = 'SQL', binds = []) + type_casted_binds = binds.map { |col, val| + [col, type_cast(val, col)] + } + log(sql, name, type_casted_binds) do + cursor = nil + cached = false + if without_prepared_statement?(binds) + cursor = @connection.prepare(sql) + else + unless @statements.key? sql + @statements[sql] = @connection.prepare(sql) + end + + cursor = @statements[sql] + + binds.each_with_index do |bind, i| + col, val = bind + cursor.bind_param(i + 1, type_cast(val, col), col) + end + + cached = true + end + + cursor.exec + + if name == 'EXPLAIN' and sql =~ /^EXPLAIN/ + res = true + else + columns = cursor.get_col_names.map do |col_name| + @connection.oracle_downcase(col_name) + end + rows = [] + fetch_options = {:get_lob_value => (name != 'Writable Large Object')} + while row = cursor.fetch(fetch_options) + rows << row + end + res = ActiveRecord::Result.new(columns, rows) + end + + cursor.close unless cached + res + end + end + + def supports_statement_cache? + true + end + + def supports_explain? + true + end + + def explain(arel, binds = []) + sql = "EXPLAIN PLAN FOR #{to_sql(arel, binds)}" + return if sql =~ /FROM all_/ + if ORACLE_ENHANCED_CONNECTION == :jdbc + exec_query(sql, 'EXPLAIN', binds) + else + exec_query(sql, 'EXPLAIN') + end + select_values("SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY)", 'EXPLAIN').join("\n") + end + + # Returns an array of arrays containing the field values. + # Order is the same as that returned by #columns. + def select_rows(sql, name = nil, binds = []) + exec_query(sql, name, binds).rows + end + + # Executes an INSERT statement and returns the new record's ID + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + # if primary key value is already prefetched from sequence + # or if there is no primary key + if id_value || pk.nil? + execute(sql, name) + return id_value + end + + sql_with_returning = sql + @connection.returning_clause(quote_column_name(pk)) + log(sql, name) do + @connection.exec_with_returning(sql_with_returning) + end + end + protected :insert_sql + + # New method in ActiveRecord 3.1 + # Will add RETURNING clause in case of trigger generated primary keys + def sql_for_insert(sql, pk, id_value, sequence_name, binds) + unless id_value || pk.nil? || (defined?(CompositePrimaryKeys) && pk.kind_of?(CompositePrimaryKeys::CompositeKeys)) + sql = "#{sql} RETURNING #{quote_column_name(pk)} INTO :returning_id" + returning_id_col = new_column("returning_id", nil, Type::Value.new, "number", true, "dual", true, true) + (binds = binds.dup) << [returning_id_col, nil] + end + [sql, binds] + end + + # New method in ActiveRecord 3.1 + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + type_casted_binds = binds.map { |col, val| + [col, type_cast(val, col)] + } + log(sql, name, type_casted_binds) do + returning_id_col = returning_id_index = nil + if without_prepared_statement?(binds) + cursor = @connection.prepare(sql) + else + unless @statements.key? (sql) + @statements[sql] = @connection.prepare(sql) + end + + cursor = @statements[sql] + + binds.each_with_index do |bind, i| + col, val = bind + if col.returning_id? + returning_id_col = [col] + returning_id_index = i + 1 + cursor.bind_returning_param(returning_id_index, Integer) + else + cursor.bind_param(i + 1, type_cast(val, col), col) + end + end + end + + cursor.exec_update + + rows = [] + if returning_id_index + returning_id = cursor.get_returning_param(returning_id_index, Integer) + rows << [returning_id] + end + ActiveRecord::Result.new(returning_id_col || [], rows) + end + end + + # New method in ActiveRecord 3.1 + def exec_update(sql, name, binds) + log(sql, name, binds) do + cached = false + if without_prepared_statement?(binds) + cursor = @connection.prepare(sql) + else + cursor = if @statements.key?(sql) + @statements[sql] + else + @statements[sql] = @connection.prepare(sql) + end + + binds.each_with_index do |bind, i| + col, val = bind + cursor.bind_param(i + 1, type_cast(val, col), col) + end + cached = true + end + + res = cursor.exec_update + cursor.close unless cached + res + end + end + + alias :exec_delete :exec_update + + def begin_db_transaction #:nodoc: + @connection.autocommit = false + end + + def transaction_isolation_levels + # Oracle database supports `READ COMMITTED` and `SERIALIZABLE` + # No read uncommitted nor repeatable read supppoted + # http://docs.oracle.com/cd/E11882_01/server.112/e26088/statements_10005.htm#SQLRF55422 + { + read_committed: "READ COMMITTED", + serializable: "SERIALIZABLE" + } + end + + def begin_isolated_db_transaction(isolation) + begin_db_transaction + execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}" + end + + def commit_db_transaction #:nodoc: + @connection.commit + ensure + @connection.autocommit = true + end + + def exec_rollback_db_transaction #:nodoc: + @connection.rollback + ensure + @connection.autocommit = true + end + + def create_savepoint(name = current_savepoint_name) #:nodoc: + execute("SAVEPOINT #{name}") + end + + def exec_rollback_to_savepoint(name = current_savepoint_name) #:nodoc: + execute("ROLLBACK TO #{name}") + end + + def release_savepoint(name = current_savepoint_name) #:nodoc: + # there is no RELEASE SAVEPOINT statement in Oracle + end + + # Returns default sequence name for table. + # Will take all or first 26 characters of table name and append _seq suffix + def default_sequence_name(table_name, primary_key = nil) + table_name.to_s.gsub /(^|\.)([\w$-]{1,#{sequence_name_length-4}})([\w$-]*)$/, '\1\2_seq' + end + + # Inserts the given fixture into the table. Overridden to properly handle lobs. + def insert_fixture(fixture, table_name) #:nodoc: + super + + if ActiveRecord::Base.pluralize_table_names + klass = table_name.to_s.singularize.camelize + else + klass = table_name.to_s.camelize + end + + klass = klass.constantize rescue nil + if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base) + write_lobs(table_name, klass, fixture, klass.lob_columns) + end + end + + private + + def select(sql, name = nil, binds = []) + exec_query(sql, name, binds) + end + + end + end + end +end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_database_tasks.rb b/lib/active_record/connection_adapters/oracle_enhanced/database_tasks.rb similarity index 100% rename from lib/active_record/connection_adapters/oracle_enhanced_database_tasks.rb rename to lib/active_record/connection_adapters/oracle_enhanced/database_tasks.rb diff --git a/lib/active_record/connection_adapters/oracle_enhanced/dirty.rb b/lib/active_record/connection_adapters/oracle_enhanced/dirty.rb new file mode 100644 index 000000000..038b9ed1d --- /dev/null +++ b/lib/active_record/connection_adapters/oracle_enhanced/dirty.rb @@ -0,0 +1,40 @@ +module ActiveRecord #:nodoc: + module ConnectionAdapters #:nodoc: + module OracleEnhancedDirty #:nodoc: + + module InstanceMethods #:nodoc: + private + + def _field_changed?(attr, old_value) + new_value = read_attribute(attr) + raw_value = read_attribute_before_type_cast(attr) + + if self.class.columns_hash.include?(attr.to_s) + column = column_for_attribute(attr) + + # Oracle stores empty string '' as NULL + # therefore need to convert empty string value to nil if old value is nil + if column.type == :string && column.null && old_value.nil? + new_value = nil if new_value == '' + end + column.changed?(old_value, new_value, raw_value) + else + new_value != old_value + end + end + + def non_zero?(value) + value !~ /\A0+(\.0+)?\z/ + end + + end + + end + end +end + +if ActiveRecord::Base.method_defined?(:changed?) + ActiveRecord::Base.class_eval do + include ActiveRecord::ConnectionAdapters::OracleEnhancedDirty::InstanceMethods + end +end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb b/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb similarity index 98% rename from lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb rename to lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb index 1b83623fc..583faebfa 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb @@ -159,8 +159,14 @@ def new_connection(config) self.autocommit = true - # default schema owner - @owner = username.upcase unless username.nil? + schema = config[:schema] && config[:schema].to_s + if schema.blank? + # default schema owner + @owner = username.upcase unless username.nil? + else + exec "alter session set current_schema = #{schema}" + @owner = schema + end @raw_connection end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb b/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb similarity index 98% rename from lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb rename to lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb index 138113d0e..c9d749e29 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb @@ -25,7 +25,9 @@ class OracleEnhancedOCIConnection < OracleEnhancedConnection #:nodoc: def initialize(config) @raw_connection = OCI8EnhancedAutoRecover.new(config, OracleEnhancedOCIFactory) # default schema owner - @owner = config[:username].to_s.upcase + @owner = config[:schema] + @owner ||= config[:username] + @owner = @owner.to_s.upcase end def raw_oci_connection @@ -306,6 +308,7 @@ def self.new_connection(config) username = config[:username] && config[:username].to_s password = config[:password] && config[:password].to_s database = config[:database] && config[:database].to_s + schema = config[:schema] && config[:schema].to_s host, port = config[:host], config[:port] privilege = config[:privilege] && config[:privilege].to_sym async = config[:allow_concurrency] @@ -333,6 +336,7 @@ def self.new_connection(config) conn.prefetch_rows = prefetch_rows conn.exec "alter session set cursor_sharing = #{cursor_sharing}" rescue nil conn.exec "alter session set time_zone = '#{time_zone}'" unless time_zone.blank? + conn.exec "alter session set current_schema = #{schema}" unless schema.blank? # Initialize NLS parameters OracleEnhancedAdapter::DEFAULT_NLS_PARAMETERS.each do |key, default_value| diff --git a/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb b/lib/active_record/connection_adapters/oracle_enhanced/procedures.rb similarity index 96% rename from lib/active_record/connection_adapters/oracle_enhanced_procedures.rb rename to lib/active_record/connection_adapters/oracle_enhanced/procedures.rb index 60b90b8fd..f9c25051e 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/procedures.rb @@ -150,9 +150,7 @@ def _update_record(attribute_names = @attributes.keys) end # update just dirty attributes if partial_writes? - # Serialized attributes should always be written in case they've been - # changed in place. - update_using_custom_method(changed | (attributes.keys & self.class.serialized_attributes.keys)) + update_using_custom_method(changed | attributes.keys) else update_using_custom_method(attribute_names) end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_schema_creation.rb b/lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb similarity index 55% rename from lib/active_record/connection_adapters/oracle_enhanced_schema_creation.rb rename to lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb index 99d642b4c..3cc5ca6e5 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_schema_creation.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb @@ -1,53 +1,47 @@ module ActiveRecord module ConnectionAdapters - class OracleEnhancedAdapter < AbstractAdapter + module OracleEnhanced class SchemaCreation < AbstractAdapter::SchemaCreation private def visit_ColumnDefinition(o) - if o.type.to_sym == :virtual - sql_type = type_to_sql(o.default[:type], o.limit, o.precision, o.scale) if o.default[:type] - "#{quote_column_name(o.name)} #{sql_type} AS (#{o.default[:as]})" - else - super + case + when o.type.to_sym == :virtual + sql_type = type_to_sql(o.default[:type], o.limit, o.precision, o.scale) if o.default[:type] + return "#{quote_column_name(o.name)} #{sql_type} AS (#{o.default[:as]})" + when [:blob, :clob].include?(sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale).downcase.to_sym) + if (tablespace = default_tablespace_for(sql_type)) + @lob_tablespaces ||= {} + @lob_tablespaces[o.name] = tablespace + end end + super end def visit_TableDefinition(o) - tablespace = tablespace_for(:table, o.options[:tablespace]) create_sql = "CREATE#{' GLOBAL TEMPORARY' if o.temporary} TABLE " create_sql << "#{quote_table_name(o.name)} (" create_sql << o.columns.map { |c| accept c }.join(', ') create_sql << ")" + unless o.temporary + @lob_tablespaces.each do |lob_column, tablespace| + create_sql << " LOB (#{quote_column_name(lob_column)}) STORE AS (TABLESPACE #{tablespace}) \n" + end if defined?(@lob_tablespaces) create_sql << " ORGANIZATION #{o.options[:organization]}" if o.options[:organization] - create_sql << "#{tablespace}" + if (tablespace = o.options[:tablespace] || default_tablespace_for(:table)) + create_sql << " TABLESPACE #{tablespace}" + end end create_sql << " #{o.options[:options]}" create_sql end - def tablespace_for(obj_type, tablespace_option, table_name=nil, column_name=nil) - tablespace_sql = '' - if tablespace = (tablespace_option || default_tablespace_for(obj_type)) - tablespace_sql << if [:blob, :clob].include?(obj_type.to_sym) - " LOB (#{quote_column_name(column_name)}) STORE AS #{column_name.to_s[0..10]}_#{table_name.to_s[0..14]}_ls (TABLESPACE #{tablespace})" - else - " TABLESPACE #{tablespace}" - end - end - tablespace_sql - end - def default_tablespace_for(type) (ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces[type] || ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces[native_database_types[type][:name]]) rescue nil end - def foreign_key_definition(to_table, options = {}) - @conn.foreign_key_definition(to_table, options) - end - def add_column_options!(sql, options) type = options[:type] || ((column = options[:column]) && column.type) type = type && type.to_sym @@ -56,8 +50,7 @@ def add_column_options!(sql, options) if type == :text sql << " DEFAULT #{@conn.quote(options[:default])}" else - # from abstract adapter - sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" + sql << " DEFAULT #{quote_value(options[:default], options[:column])}" end end # must explicitly add NULL or NOT NULL to allow change_column to work on migrations @@ -72,18 +65,24 @@ def add_column_options!(sql, options) end end - # This method does not exist in SchemaCreation at Rails 4.0 - # It can be removed only when Oracle enhanced adapter supports Rails 4.1 and higher - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) + def action_sql(action, dependency) + if action == 'UPDATE' + raise ArgumentError, <<-MSG.strip_heredoc + '#{action}' is not supported by Oracle + MSG + end + case dependency + when :nullify then "ON #{action} SET NULL" + when :cascade then "ON #{action} CASCADE" + else + raise ArgumentError, <<-MSG.strip_heredoc + '#{dependency}' is not supported for #{action} + Supported values are: :nullify, :cascade + MSG + end end end - - def schema_creation - SchemaCreation.new self - end - end end end diff --git a/lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb b/lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb new file mode 100644 index 000000000..98c4cfb96 --- /dev/null +++ b/lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb @@ -0,0 +1,95 @@ +module ActiveRecord + module ConnectionAdapters + #TODO: Overriding `aliased_types` cause another database adapter behavior changes + #It should be addressed by supporting `create_table_definition` + class TableDefinition + private + def aliased_types(name, fallback) + fallback + end + end + + module OracleEnhanced + + class ForeignKeyDefinition < ActiveRecord::ConnectionAdapters::ForeignKeyDefinition + def name + if options[:name].length > OracleEnhancedAdapter::IDENTIFIER_MAX_LENGTH + ActiveSupport::Deprecation.warn "Foreign key name #{options[:name]} is too long. It will not get shorten in later version of Oracle enhanced adapter" + 'c'+Digest::SHA1.hexdigest(options[:name])[0,OracleEnhancedAdapter::IDENTIFIER_MAX_LENGTH-1] + else + options[:name] + end + end + end + + class SynonymDefinition < Struct.new(:name, :table_owner, :table_name, :db_link) #:nodoc: + end + + class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition + attr_accessor :table, :name, :unique, :type, :parameters, :statement_parameters, :tablespace, :columns + + def initialize(table, name, unique, type, parameters, statement_parameters, tablespace, columns) + @table = table + @name = name + @unique = unique + @type = type + @parameters = parameters + @statement_parameters = statement_parameters + @tablespace = tablespace + @columns = columns + super(table, name, unique, columns, nil, nil, nil, nil) + end + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + + def raw(name, options={}) + column(name, :raw, options) + end + + def virtual(* args) + options = args.extract_options! + column_names = args + column_names.each { |name| column(name, :virtual, options) } + end + + def column(name, type, options = {}) + if type == :virtual + default = {:type => options[:type]} + if options[:as] + default[:as] = options[:as] + elsif options[:default] + warn "[DEPRECATION] virtual column `:default` option is deprecated. Please use `:as` instead." + default[:as] = options[:default] + else + raise "No virtual column definition found." + end + options[:default] = default + end + super(name, type, options) + end + + end + + class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable + def add_foreign_key(to_table, options) + @foreign_key_adds << OracleEnhanced::ForeignKeyDefinition.new(name, to_table, options) + end + end + + class Table < ActiveRecord::ConnectionAdapters::Table + def foreign_key(to_table, options = {}) + ActiveSupport::Deprecation.warn "`foreign_key` option will be deprecated. Please use `references` option" + to_table = to_table.to_s.pluralize if ActiveRecord::Base.pluralize_table_names + @base.add_foreign_key(@name, to_table, options) + end + + def remove_foreign_key(options = {}) + ActiveSupport::Deprecation.warn "`remove_foreign_key` option will be deprecated. Please use `remove_references` option" + @base.remove_foreign_key(@name, options) + end + end + + end + end +end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb b/lib/active_record/connection_adapters/oracle_enhanced/schema_dumper.rb similarity index 85% rename from lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb rename to lib/active_record/connection_adapters/oracle_enhanced/schema_dumper.rb index 422912597..4d9d63393 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/schema_dumper.rb @@ -7,6 +7,7 @@ def self.included(base) #:nodoc: private alias_method_chain :tables, :oracle_enhanced alias_method_chain :indexes, :oracle_enhanced + alias_method_chain :foreign_keys, :oracle_enhanced end end @@ -56,37 +57,8 @@ def primary_key_trigger(table_name, stream) end end - def foreign_keys(table_name, stream) - if @connection.respond_to?(:foreign_keys) && (foreign_keys = @connection.foreign_keys(table_name)).any? - add_foreign_key_statements = foreign_keys.map do |foreign_key| - statement_parts = [ ('add_foreign_key ' + foreign_key.from_table.inspect) ] - statement_parts << foreign_key.to_table.inspect - - if foreign_key.options[:columns].size == 1 - column = foreign_key.options[:columns].first - if column != "#{foreign_key.to_table.singularize}_id" - statement_parts << ('column: ' + column.inspect) - end - - if foreign_key.options[:references].first != 'id' - statement_parts << ('primary_key: ' + foreign_key.options[:references].first.inspect) - end - else - statement_parts << ('columns: ' + foreign_key.options[:columns].inspect) - end - - statement_parts << ('name: ' + foreign_key.options[:name].inspect) - - unless foreign_key.options[:dependent].blank? - statement_parts << ('dependent: ' + foreign_key.options[:dependent].inspect) - end - - ' ' + statement_parts.join(', ') - end - - stream.puts add_foreign_key_statements.sort.join("\n") - stream.puts - end + def foreign_keys_with_oracle_enhanced(table_name, stream) + return foreign_keys_without_oracle_enhanced(table_name, stream) end def synonyms(stream) @@ -166,7 +138,7 @@ def oracle_enhanced_table(table, stream) else tbl.print ", id: false" end - tbl.print ", force: true" + tbl.print ", force: :cascade" tbl.puts " do |t|" # then dump all non-primary key columns diff --git a/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb b/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb new file mode 100644 index 000000000..a9bcdba1c --- /dev/null +++ b/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb @@ -0,0 +1,548 @@ +# -*- coding: utf-8 -*- +require 'digest/sha1' + +module ActiveRecord + module ConnectionAdapters + module OracleEnhanced + module SchemaStatements + # SCHEMA STATEMENTS ======================================== + # + # see: abstract/schema_statements.rb + + # Additional options for +create_table+ method in migration files. + # + # You can specify individual starting value in table creation migration file, e.g.: + # + # create_table :users, :sequence_start_value => 100 do |t| + # # ... + # end + # + # You can also specify other sequence definition additional parameters, e.g.: + # + # create_table :users, :sequence_start_value => “100 NOCACHE INCREMENT BY 10” do |t| + # # ... + # end + # + # Create primary key trigger (so that you can skip primary key value in INSERT statement). + # By default trigger name will be "table_name_pkt", you can override the name with + # :trigger_name option (but it is not recommended to override it as then this trigger will + # not be detected by ActiveRecord model and it will still do prefetching of sequence value). + # Example: + # + # create_table :users, :primary_key_trigger => true do |t| + # # ... + # end + # + # It is possible to add table and column comments in table creation migration files: + # + # create_table :employees, :comment => “Employees and contractors” do |t| + # t.string :first_name, :comment => “Given name” + # t.string :last_name, :comment => “Surname” + # end + + def create_table(table_name, options = {}) + create_sequence = options[:id] != false + column_comments = {} + temporary = options.delete(:temporary) + additional_options = options + td = create_table_definition table_name, temporary, additional_options + td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false + + # store that primary key was defined in create_table block + unless create_sequence + class << td + attr_accessor :create_sequence + def primary_key(*args) + self.create_sequence = true + super(*args) + end + end + end + + # store column comments + class << td + attr_accessor :column_comments + def column(name, type, options = {}) + if options[:comment] + self.column_comments ||= {} + self.column_comments[name] = options[:comment] + end + super(name, type, options) + end + end + + yield td if block_given? + create_sequence = create_sequence || td.create_sequence + column_comments = td.column_comments if td.column_comments + tablespace = tablespace_for(:table, options[:tablespace]) + + if options[:force] && table_exists?(table_name) + drop_table(table_name, options) + end + + execute schema_creation.accept td + + create_sequence_and_trigger(table_name, options) if create_sequence + + add_table_comment table_name, options[:comment] + column_comments.each do |column_name, comment| + add_comment table_name, column_name, comment + end + td.indexes.each_pair { |c,o| add_index table_name, c, o } + + td.foreign_keys.each_pair do |other_table_name, foreign_key_options| + add_foreign_key(table_name, other_table_name, foreign_key_options) + end + end + + def create_table_definition(name, temporary, options) + ActiveRecord::ConnectionAdapters::OracleEnhanced::TableDefinition.new native_database_types, name, temporary, options + end + + def rename_table(table_name, new_name) #:nodoc: + if new_name.to_s.length > table_name_length + raise ArgumentError, "New table name '#{new_name}' is too long; the limit is #{table_name_length} characters" + end + if "#{new_name}_seq".to_s.length > sequence_name_length + raise ArgumentError, "New sequence name '#{new_name}_seq' is too long; the limit is #{sequence_name_length} characters" + end + execute "RENAME #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" + execute "RENAME #{quote_table_name("#{table_name}_seq")} TO #{quote_table_name("#{new_name}_seq")}" rescue nil + + rename_table_indexes(table_name, new_name) + end + + def drop_table(table_name, options = {}) #:nodoc: + execute "DROP TABLE #{quote_table_name(table_name)}#{' CASCADE CONSTRAINTS' if options[:force] == :cascade}" + seq_name = options[:sequence_name] || default_sequence_name(table_name) + execute "DROP SEQUENCE #{quote_table_name(seq_name)}" rescue nil + rescue ActiveRecord::StatementInvalid => e + raise e unless options[:if_exists] + ensure + clear_table_columns_cache(table_name) + self.all_schema_indexes = nil + end + + def dump_schema_information #:nodoc: + sm_table = ActiveRecord::Migrator.schema_migrations_table_name + migrated = select_values("SELECT version FROM #{sm_table} ORDER BY version") + join_with_statement_token(migrated.map{|v| "INSERT INTO #{sm_table} (version) VALUES ('#{v}')" }) + end + + def initialize_schema_migrations_table + sm_table = ActiveRecord::Migrator.schema_migrations_table_name + + unless table_exists?(sm_table) + index_name = "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" + if index_name.length > index_name_length + truncate_to = index_name_length - index_name.to_s.length - 1 + truncated_name = "unique_schema_migrations"[0..truncate_to] + index_name = "#{Base.table_name_prefix}#{truncated_name}#{Base.table_name_suffix}" + end + + create_table(sm_table, :id => false) do |schema_migrations_table| + schema_migrations_table.column :version, :string, :null => false + end + add_index sm_table, :version, :unique => true, :name => index_name + + # Backwards-compatibility: if we find schema_info, assume we've + # migrated up to that point: + si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix + if table_exists?(si_table) + ActiveSupport::Deprecation.warn "Usage of the schema table `#{si_table}` is deprecated. Please switch to using `schema_migrations` table" + + old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i + assume_migrated_upto_version(old_version) + drop_table(si_table) + end + end + end + + def update_table_definition(table_name, base) #:nodoc: + OracleEnhanced::Table.new(table_name, base) + end + + def add_index(table_name, column_name, options = {}) #:nodoc: + index_name, index_type, quoted_column_names, tablespace, index_options = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})#{tablespace} #{index_options}" + if index_type == 'UNIQUE' + unless quoted_column_names =~ /\(.*\)/ + execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{quote_column_name(index_name)} #{index_type} (#{quoted_column_names})" + end + end + ensure + self.all_schema_indexes = nil + end + + def add_index_options(table_name, column_name, options = {}) #:nodoc: + column_names = Array(column_name) + index_name = index_name(table_name, column: column_names) + + options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :tablespace, :options, :using) + + index_type = options[:unique] ? "UNIQUE" : "" + index_name = options[:name].to_s if options.key?(:name) + tablespace = tablespace_for(:index, options[:tablespace]) + max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length + #TODO: This option is used for NOLOGGING, needs better argumetn name + index_options = options[:options] + + if index_name.to_s.length > max_index_length + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" + end + if index_name_exists?(table_name, index_name, false) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" + end + + quoted_column_names = column_names.map { |e| quote_column_name_or_expression(e) }.join(", ") + [index_name, index_type, quoted_column_names, tablespace, index_options] + end + + # Remove the given index from the table. + # Gives warning if index does not exist + def remove_index(table_name, options = {}) #:nodoc: + index_name = index_name(table_name, options) + unless index_name_exists?(table_name, index_name, true) + # sometimes options can be String or Array with column names + options = {} unless options.is_a?(Hash) + if options.has_key? :name + options_without_column = options.dup + options_without_column.delete :column + index_name_without_column = index_name(table_name, options_without_column) + return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) + end + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + end + remove_index!(table_name, index_name) + end + + # clear cached indexes when removing index + def remove_index!(table_name, index_name) #:nodoc: + #TODO: It should execute only when index_type == "UNIQUE" + execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(index_name)}" rescue nil + execute "DROP INDEX #{quote_column_name(index_name)}" + ensure + self.all_schema_indexes = nil + end + + # returned shortened index name if default is too large + def index_name(table_name, options) #:nodoc: + default_name = super(table_name, options).to_s + # sometimes options can be String or Array with column names + options = {} unless options.is_a?(Hash) + identifier_max_length = options[:identifier_max_length] || index_name_length + return default_name if default_name.length <= identifier_max_length + + # remove 'index', 'on' and 'and' keywords + shortened_name = "i_#{table_name}_#{Array(options[:column]) * '_'}" + + # leave just first three letters from each word + if shortened_name.length > identifier_max_length + shortened_name = shortened_name.split('_').map{|w| w[0,3]}.join('_') + end + # generate unique name using hash function + if shortened_name.length > identifier_max_length + shortened_name = 'i'+Digest::SHA1.hexdigest(default_name)[0,identifier_max_length-1] + end + @logger.warn "#{adapter_name} shortened default index name #{default_name} to #{shortened_name}" if @logger + shortened_name + end + + # Verify the existence of an index with a given name. + # + # The default argument is returned if the underlying implementation does not define the indexes method, + # as there's no way to determine the correct answer in that case. + # + # Will always query database and not index cache. + def index_name_exists?(table_name, index_name, default) + (owner, table_name, db_link) = @connection.describe(table_name) + result = select_value(<<-SQL) + SELECT 1 FROM all_indexes#{db_link} i + WHERE i.owner = '#{owner}' + AND i.table_owner = '#{owner}' + AND i.table_name = '#{table_name}' + AND i.index_name = '#{index_name.to_s.upcase}' + SQL + result == 1 + end + + def rename_index(table_name, old_name, new_name) #:nodoc: + unless index_name_exists?(table_name, old_name, true) + raise ArgumentError, "Index name '#{old_name}' on table '#{table_name}' does not exist" + end + if new_name.length > allowed_index_name_length + raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" + end + execute "ALTER INDEX #{quote_column_name(old_name)} rename to #{quote_column_name(new_name)}" + ensure + self.all_schema_indexes = nil + end + + def add_column(table_name, column_name, type, options = {}) #:nodoc: + if type.to_sym == :virtual + type = options[:type] + end + type = aliased_types(type.to_s, type) + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} " + add_column_sql << type_to_sql(type, options[:limit], options[:precision], options[:scale]) if type + + add_column_options!(add_column_sql, options.merge(:type=>type, :column_name=>column_name, :table_name=>table_name)) + + add_column_sql << tablespace_for((type_to_sql(type).downcase.to_sym), nil, table_name, column_name) if type + + execute(add_column_sql) + + create_sequence_and_trigger(table_name, options) if type && type.to_sym == :primary_key + ensure + clear_table_columns_cache(table_name) + end + + def aliased_types(name, fallback) + fallback + end + + def change_column_default(table_name, column_name, default) #:nodoc: + execute "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} DEFAULT #{quote(default)}" + ensure + clear_table_columns_cache(table_name) + end + + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: + column = column_for(table_name, column_name) + + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + + change_column table_name, column_name, column.sql_type, :null => null + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + column = column_for(table_name, column_name) + + # remove :null option if its value is the same as current column definition + # otherwise Oracle will raise error + if options.has_key?(:null) && options[:null] == column.null + options[:null] = nil + end + if type.to_sym == :virtual + type = options[:type] + end + change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} " + change_column_sql << "#{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" if type + + add_column_options!(change_column_sql, options.merge(:type=>type, :column_name=>column_name, :table_name=>table_name)) + + change_column_sql << tablespace_for((type_to_sql(type).downcase.to_sym), nil, options[:table_name], options[:column_name]) if type + + execute(change_column_sql) + ensure + clear_table_columns_cache(table_name) + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} to #{quote_column_name(new_column_name)}" + self.all_schema_indexes = nil + rename_column_indexes(table_name, column_name, new_column_name) + ensure + clear_table_columns_cache(table_name) + end + + def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc: + execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)} CASCADE CONSTRAINTS" + ensure + clear_table_columns_cache(table_name) + self.all_schema_indexes = nil + end + + def add_comment(table_name, column_name, comment) #:nodoc: + return if comment.blank? + execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{column_name} IS '#{comment}'" + end + + def add_table_comment(table_name, comment) #:nodoc: + return if comment.blank? + execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS '#{comment}'" + end + + def table_comment(table_name) #:nodoc: + (owner, table_name, db_link) = @connection.describe(table_name) + select_value <<-SQL + SELECT comments FROM all_tab_comments#{db_link} + WHERE owner = '#{owner}' + AND table_name = '#{table_name}' + SQL + end + + def column_comment(table_name, column_name) #:nodoc: + (owner, table_name, db_link) = @connection.describe(table_name) + select_value <<-SQL + SELECT comments FROM all_col_comments#{db_link} + WHERE owner = '#{owner}' + AND table_name = '#{table_name}' + AND column_name = '#{column_name.upcase}' + SQL + end + + # Maps logical Rails types to Oracle-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: + # Ignore options for :text and :binary columns + return super(type, nil, nil, nil) if ['text', 'binary'].include?(type.to_s) + + super + end + + def tablespace(table_name) + select_value <<-SQL + SELECT tablespace_name + FROM user_tables + WHERE table_name='#{table_name.to_s.upcase}' + SQL + end + + def add_foreign_key(from_table, to_table, options = {}) + if options[:dependent] + ActiveSupport::Deprecation.warn "`:dependent` option will be deprecated. Please use `:on_delete` option" + end + case options[:dependent] + when :delete then options[:on_delete] = :cascade + when :nullify then options[:on_delete] = :nullify + else + end + + super + end + + def remove_foreign_key(from_table, options_or_to_table = {}) + super + end + + # get table foreign keys for schema dump + def foreign_keys(table_name) #:nodoc: + (owner, desc_table_name, db_link) = @connection.describe(table_name) + + fk_info = select_all(<<-SQL, 'Foreign Keys') + SELECT r.table_name to_table + ,rc.column_name references_column + ,cc.column_name + ,c.constraint_name name + ,c.delete_rule + FROM user_constraints#{db_link} c, user_cons_columns#{db_link} cc, + user_constraints#{db_link} r, user_cons_columns#{db_link} rc + WHERE c.owner = '#{owner}' + AND c.table_name = '#{desc_table_name}' + AND c.constraint_type = 'R' + AND cc.owner = c.owner + AND cc.constraint_name = c.constraint_name + AND r.constraint_name = c.r_constraint_name + AND r.owner = c.owner + AND rc.owner = r.owner + AND rc.constraint_name = r.constraint_name + AND rc.position = cc.position + ORDER BY name, to_table, column_name, references_column + SQL + + fk_info.map do |row| + options = { + column: oracle_downcase(row['column_name']), + name: oracle_downcase(row['name']), + primary_key: oracle_downcase(row['references_column']) + } + options[:on_delete] = extract_foreign_key_action(row['delete_rule']) + OracleEnhanced::ForeignKeyDefinition.new(oracle_downcase(table_name), oracle_downcase(row['to_table']), options) + end + end + + def extract_foreign_key_action(specifier) # :nodoc: + case specifier + when 'CASCADE'; :cascade + when 'SET NULL'; :nullify + end + end + + # REFERENTIAL INTEGRITY ==================================== + + def disable_referential_integrity(&block) #:nodoc: + sql_constraints = <<-SQL + SELECT constraint_name, owner, table_name + FROM user_constraints + WHERE constraint_type = 'R' + AND status = 'ENABLED' + SQL + old_constraints = select_all(sql_constraints) + begin + old_constraints.each do |constraint| + execute "ALTER TABLE #{constraint["table_name"]} DISABLE CONSTRAINT #{constraint["constraint_name"]}" + end + yield + ensure + old_constraints.each do |constraint| + execute "ALTER TABLE #{constraint["table_name"]} ENABLE CONSTRAINT #{constraint["constraint_name"]}" + end + end + end + + private + + def create_alter_table(name) + OracleEnhanced::AlterTable.new create_table_definition(name, false, {}) + end + + def tablespace_for(obj_type, tablespace_option, table_name=nil, column_name=nil) + tablespace_sql = '' + if tablespace = (tablespace_option || default_tablespace_for(obj_type)) + tablespace_sql << if [:blob, :clob].include?(obj_type.to_sym) + " LOB (#{quote_column_name(column_name)}) STORE AS #{column_name.to_s[0..10]}_#{table_name.to_s[0..14]}_ls (TABLESPACE #{tablespace})" + else + " TABLESPACE #{tablespace}" + end + end + tablespace_sql + end + + def default_tablespace_for(type) + (default_tablespaces[type] || default_tablespaces[native_database_types[type][:name]]) rescue nil + end + + + def column_for(table_name, column_name) + unless column = columns(table_name).find { |c| c.name == column_name.to_s } + raise "No such column: #{table_name}.#{column_name}" + end + column + end + + def create_sequence_and_trigger(table_name, options) + seq_name = options[:sequence_name] || default_sequence_name(table_name) + seq_start_value = options[:sequence_start_value] || default_sequence_start_value + execute "CREATE SEQUENCE #{quote_table_name(seq_name)} START WITH #{seq_start_value}" + + create_primary_key_trigger(table_name, options) if options[:primary_key_trigger] + end + + def create_primary_key_trigger(table_name, options) + seq_name = options[:sequence_name] || default_sequence_name(table_name) + trigger_name = options[:trigger_name] || default_trigger_name(table_name) + primary_key = options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize) + execute compress_lines(<<-SQL) + CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)} + BEFORE INSERT ON #{quote_table_name(table_name)} FOR EACH ROW + BEGIN + IF inserting THEN + IF :new.#{quote_column_name(primary_key)} IS NULL THEN + SELECT #{quote_table_name(seq_name)}.NEXTVAL INTO :new.#{quote_column_name(primary_key)} FROM dual; + END IF; + END IF; + END; + SQL + end + + def default_trigger_name(table_name) + # truncate table name if necessary to fit in max length of identifier + "#{table_name.to_s[0,table_name_length-4]}_pkt" + end + + end + end + end +end diff --git a/lib/active_record/connection_adapters/oracle_enhanced/schema_statements_ext.rb b/lib/active_record/connection_adapters/oracle_enhanced/schema_statements_ext.rb new file mode 100644 index 000000000..0fde27fe7 --- /dev/null +++ b/lib/active_record/connection_adapters/oracle_enhanced/schema_statements_ext.rb @@ -0,0 +1,74 @@ +require 'digest/sha1' + +module ActiveRecord + module ConnectionAdapters + module OracleEnhancedSchemaStatementsExt + # Create primary key trigger (so that you can skip primary key value in INSERT statement). + # By default trigger name will be "table_name_pkt", you can override the name with + # :trigger_name option (but it is not recommended to override it as then this trigger will + # not be detected by ActiveRecord model and it will still do prefetching of sequence value). + # + # add_primary_key_trigger :users + # + # You can also create primary key trigger using +create_table+ with :primary_key_trigger + # option: + # + # create_table :users, :primary_key_trigger => true do |t| + # # ... + # end + # + def add_primary_key_trigger(table_name, options={}) + # call the same private method that is used for create_table :primary_key_trigger => true + create_primary_key_trigger(table_name, options) + end + + def table_definition_tablespace + # TODO: Support specifying an :index_tablespace option in create_table? + tablespace_sql = '' + if tablespace = default_tablespace_for(:index) + tablespace_sql << " USING INDEX TABLESPACE #{tablespace}" + end + tablespace_sql + end + + # Add synonym to existing table or view or sequence. Can be used to create local synonym to + # remote table in other schema or in other database + # Examples: + # + # add_synonym :posts, "blog.posts" + # add_synonym :posts_seq, "blog.posts_seq" + # add_synonym :employees, "hr.employees@dblink", :force => true + # + def add_synonym(name, table_name, options = {}) + sql = "CREATE" + if options[:force] == true + sql << " OR REPLACE" + end + sql << " SYNONYM #{quote_table_name(name)} FOR #{quote_table_name(table_name)}" + execute sql + end + + # Remove existing synonym to table or view or sequence + # Example: + # + # remove_synonym :posts, "blog.posts" + # + def remove_synonym(name) + execute "DROP SYNONYM #{quote_table_name(name)}" + end + + # get synonyms for schema dump + def synonyms #:nodoc: + select_all("SELECT synonym_name, table_owner, table_name, db_link FROM all_synonyms WHERE owner = SYS_CONTEXT('userenv', 'current_schema')").collect do |row| + OracleEnhanced::SynonymDefinition.new(oracle_downcase(row['synonym_name']), + oracle_downcase(row['table_owner']), oracle_downcase(row['table_name']), oracle_downcase(row['db_link'])) + end + end + + end + end +end + +ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do + include ActiveRecord::ConnectionAdapters::OracleEnhancedSchemaStatementsExt +end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb b/lib/active_record/connection_adapters/oracle_enhanced/structure_dump.rb similarity index 85% rename from lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb rename to lib/active_record/connection_adapters/oracle_enhanced/structure_dump.rb index bdc2ea2bd..188186950 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/structure_dump.rb @@ -10,7 +10,7 @@ def structure_dump #:nodoc: "CREATE SEQUENCE \"#{seq}\"" end select_values("SELECT table_name FROM all_tables t - WHERE owner = SYS_CONTEXT('userenv', 'session_user') AND secondary = 'N' + WHERE owner = SYS_CONTEXT('userenv', 'current_schema') AND secondary = 'N' AND NOT EXISTS (SELECT mv.mview_name FROM all_mviews mv WHERE mv.owner = t.owner AND mv.mview_name = t.table_name) AND NOT EXISTS (SELECT mvl.log_table FROM all_mview_logs mvl WHERE mvl.log_owner = t.owner AND mvl.log_table = t.table_name) ORDER BY 1").each do |table_name| @@ -77,7 +77,7 @@ def structure_dump_primary_key(table) #:nodoc: ON a.constraint_name = c.constraint_name WHERE c.table_name = '#{table.upcase}' AND c.constraint_type = 'P' - AND c.owner = SYS_CONTEXT('userenv', 'session_user') + AND c.owner = SYS_CONTEXT('userenv', 'current_schema') SQL pks.each do |row| opts[:name] = row['constraint_name'] @@ -95,7 +95,7 @@ def structure_dump_unique_keys(table) #:nodoc: ON a.constraint_name = c.constraint_name WHERE c.table_name = '#{table.upcase}' AND c.constraint_type = 'U' - AND c.owner = SYS_CONTEXT('userenv', 'session_user') + AND c.owner = SYS_CONTEXT('userenv', 'current_schema') SQL uks.each do |uk| keys[uk['constraint_name']] ||= [] @@ -123,23 +123,45 @@ def structure_dump_indexes(table_name) #:nodoc: end def structure_dump_fk_constraints #:nodoc: - fks = select_all("SELECT table_name FROM all_tables WHERE owner = SYS_CONTEXT('userenv', 'session_user') ORDER BY 1").map do |table| + fks = select_all("SELECT table_name FROM all_tables WHERE owner = SYS_CONTEXT('userenv', 'current_schema') ORDER BY 1").map do |table| if respond_to?(:foreign_keys) && (foreign_keys = foreign_keys(table["table_name"])).any? foreign_keys.map do |fk| sql = "ALTER TABLE #{quote_table_name(fk.from_table)} ADD CONSTRAINT #{quote_column_name(fk.options[:name])} " - sql << "#{foreign_key_definition(fk.to_table, fk.options)}" + sql << "#{foreign_key_definition(fk.to_table, fk.options) << table_definition_tablespace}" end end end.flatten.compact join_with_statement_token(fks) end - def dump_schema_information #:nodoc: - sm_table = ActiveRecord::Migrator.schema_migrations_table_name - migrated = select_values("SELECT version FROM #{sm_table} ORDER BY version") - join_with_statement_token(migrated.map{|v| "INSERT INTO #{sm_table} (version) VALUES ('#{v}')" }) + def foreign_key_definition(to_table, options = {}) #:nodoc: + columns = Array(options[:column] || options[:columns]) + + if columns.size > 1 + # composite foreign key + columns_sql = columns.map {|c| quote_column_name(c)}.join(',') + references = options[:references] || columns + references_sql = references.map {|c| quote_column_name(c)}.join(',') + else + columns_sql = quote_column_name(columns.first || "#{to_table.to_s.singularize}_id") + references = options[:references] ? options[:references].first : nil + references_sql = quote_column_name(options[:primary_key] || references || "id") + end + + table_name = to_table + + sql = "FOREIGN KEY (#{columns_sql}) REFERENCES #{quote_table_name(table_name)}(#{references_sql})" + + case options[:dependent] + when :nullify + sql << " ON DELETE SET NULL" + when :delete + sql << " ON DELETE CASCADE" + end + sql end + # Extract all stored procedures, packages, synonyms and views. def structure_dump_db_stored_code #:nodoc: structure = [] @@ -147,14 +169,14 @@ def structure_dump_db_stored_code #:nodoc: FROM all_source WHERE type IN ('PROCEDURE', 'PACKAGE', 'PACKAGE BODY', 'FUNCTION', 'TRIGGER', 'TYPE') AND name NOT LIKE 'BIN$%' - AND owner = SYS_CONTEXT('userenv', 'session_user') ORDER BY type").each do |source| + AND owner = SYS_CONTEXT('userenv', 'current_schema') ORDER BY type").each do |source| ddl = "CREATE OR REPLACE \n" select_all(%Q{ SELECT text FROM all_source WHERE name = '#{source['name']}' AND type = '#{source['type']}' - AND owner = SYS_CONTEXT('userenv', 'session_user') + AND owner = SYS_CONTEXT('userenv', 'current_schema') ORDER BY line }).each do |row| ddl << row['text'] @@ -171,9 +193,9 @@ def structure_dump_db_stored_code #:nodoc: # export synonyms select_all("SELECT owner, synonym_name, table_name, table_owner FROM all_synonyms - WHERE owner = SYS_CONTEXT('userenv', 'session_user') ").each do |synonym| - structure << "CREATE OR REPLACE #{synonym['owner'] == 'PUBLIC' ? 'PUBLIC' : '' } SYNONYM #{synonym['synonym_name']} - FOR #{synonym['table_owner']}.#{synonym['table_name']}" + WHERE owner = SYS_CONTEXT('userenv', 'current_schema') ").each do |synonym| + structure << "CREATE OR REPLACE #{synonym['owner'] == 'PUBLIC' ? 'PUBLIC' : '' } SYNONYM #{synonym['synonym_name']}" + structure << " FOR #{synonym['table_owner']}.#{synonym['table_name']}" end join_with_statement_token(structure) @@ -184,7 +206,7 @@ def structure_drop #:nodoc: "DROP SEQUENCE \"#{seq}\"" end select_values("SELECT table_name from all_tables t - WHERE owner = SYS_CONTEXT('userenv', 'session_user') AND secondary = 'N' + WHERE owner = SYS_CONTEXT('userenv', 'current_schema') AND secondary = 'N' AND NOT EXISTS (SELECT mv.mview_name FROM all_mviews mv WHERE mv.owner = t.owner AND mv.mview_name = t.table_name) AND NOT EXISTS (SELECT mvl.log_table FROM all_mview_logs mvl WHERE mvl.log_owner = t.owner AND mvl.log_table = t.table_name) ORDER BY 1").each do |table| @@ -196,7 +218,7 @@ def structure_drop #:nodoc: def temp_table_drop #:nodoc: join_with_statement_token(select_values( "SELECT table_name FROM all_tables - WHERE owner = SYS_CONTEXT('userenv', 'session_user') AND secondary = 'N' AND temporary = 'Y' ORDER BY 1").map do |table| + WHERE owner = SYS_CONTEXT('userenv', 'current_schema') AND secondary = 'N' AND temporary = 'Y' ORDER BY 1").map do |table| "DROP TABLE \"#{table}\" CASCADE CONSTRAINTS" end) end diff --git a/lib/active_record/connection_adapters/oracle_enhanced/version.rb b/lib/active_record/connection_adapters/oracle_enhanced/version.rb new file mode 100644 index 000000000..ff87a3c7d --- /dev/null +++ b/lib/active_record/connection_adapters/oracle_enhanced/version.rb @@ -0,0 +1 @@ +ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter::VERSION = File.read(File.expand_path('../../../../../VERSION', __FILE__)).chomp diff --git a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb index 37170ac2e..1e3dc8cbf 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb @@ -30,9 +30,13 @@ # portions Copyright 2005 Graham Jenkins require 'active_record/connection_adapters/abstract_adapter' -require 'active_record/connection_adapters/oracle_enhanced_connection' +require 'active_record/connection_adapters/oracle_enhanced/connection' +require 'active_record/connection_adapters/oracle_enhanced/database_statements' +require 'active_record/connection_adapters/oracle_enhanced/schema_statements' +require 'active_record/connection_adapters/oracle_enhanced/column_dumper' +require 'active_record/connection_adapters/oracle_enhanced/context_index' -require 'active_record/connection_adapters/oracle_enhanced_column' +require 'active_record/connection_adapters/oracle_enhanced/column' require 'digest/sha1' @@ -131,7 +135,7 @@ def enhanced_write_lobs def record_changed_lobs @changed_lob_columns = self.class.lob_columns.select do |col| - (self.class.serialized_attributes.keys.include?(col.name) || self.send(:"#{col.name}_changed?")) && !self.class.readonly_attributes.to_a.include?(col.name) + self.attribute_changed?(col.name) && !self.class.readonly_attributes.to_a.include?(col.name) end end end @@ -218,6 +222,15 @@ module ConnectionAdapters #:nodoc: # * :nls_time_tz_format # class OracleEnhancedAdapter < AbstractAdapter + # TODO: Use relative + include ActiveRecord::ConnectionAdapters::OracleEnhanced::DatabaseStatements + include ActiveRecord::ConnectionAdapters::OracleEnhanced::SchemaStatements + include ActiveRecord::ConnectionAdapters::OracleEnhanced::ColumnDumper + include ActiveRecord::ConnectionAdapters::OracleEnhanced::ContextIndex + + def schema_creation + OracleEnhanced::SchemaCreation.new self + end ## # :singleton-method: @@ -270,13 +283,6 @@ class OracleEnhancedAdapter < AbstractAdapter cattr_accessor :emulate_dates_by_column_name self.emulate_dates_by_column_name = false - ## - # :singleton-method: - # Specify how `NUMBER` datatype columns, without precision and scale, are handled in Rails world. - # Default is :decimal and other valid option is :float. Be wary of setting it to other values. - cattr_accessor :number_datatype_coercion - self.number_datatype_coercion = :decimal - # Check column name to identify if it is Date (and not Time) column. # Is used if +emulate_dates_by_column_name+ option is set to +true+. # Override this method definition in initializer file if different Date column recognition is needed. @@ -311,7 +317,7 @@ def is_date_column?(name, table_name = nil) #:nodoc: # Is used if +emulate_integers_by_column_name+ option is set to +true+. # Override this method definition in initializer file if different Integer column recognition is needed. def self.is_integer_column?(name, table_name = nil) - !!(name =~ /(^|_)id$/i) + name =~ /(^|_)id$/i end ## @@ -326,9 +332,9 @@ def self.is_integer_column?(name, table_name = nil) # Check column name to identify if it is boolean (and not String) column. # Is used if +emulate_booleans_from_strings+ option is set to +true+. # Override this method definition in initializer file if different boolean column recognition is needed. - def self.is_boolean_column?(name, field_type, table_name = nil) - return true if ["CHAR(1)","VARCHAR2(1)"].include?(field_type) - field_type =~ /^VARCHAR2/ && (name =~ /_flag$/i || name =~ /_yn$/i) + def self.is_boolean_column?(name, sql_type, table_name = nil) + return true if ["CHAR(1)","VARCHAR2(1)"].include?(sql_type) + sql_type =~ /^VARCHAR2/ && (name =~ /_flag$/i || name =~ /_yn$/i) end # How boolean value should be quoted to String. @@ -383,21 +389,18 @@ def clear end end - class BindSubstitution < Arel::Visitors::Oracle #:nodoc: - include Arel::Visitors::BindVisitor - end - def initialize(connection, logger, config) #:nodoc: super(connection, logger) @quoted_column_names, @quoted_table_names = {}, {} @config = config @statements = StatementPool.new(connection, config.fetch(:statement_limit) { 250 }) @enable_dbms_output = false - if config.fetch(:prepared_statements) { true } - @visitor = Arel::Visitors::Oracle.new self + @visitor = Arel::Visitors::Oracle.new self + + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true else - @visitor = unprepared_visitor + @prepared_statements = false end end @@ -423,7 +426,13 @@ def supports_transaction_isolation? #:nodoc: true end - NUMBER_MAX_PRECISION = 38 + def supports_foreign_keys? + true + end + + def supports_views? + true + end #:stopdoc: DEFAULT_NLS_PARAMETERS = { @@ -448,11 +457,11 @@ def supports_transaction_isolation? #:nodoc: #:stopdoc: NATIVE_DATABASE_TYPES = { - :primary_key => "NUMBER(#{NUMBER_MAX_PRECISION}) NOT NULL PRIMARY KEY", + :primary_key => "NUMBER(38) NOT NULL PRIMARY KEY", :string => { :name => "VARCHAR2", :limit => 255 }, :text => { :name => "CLOB" }, - :integer => { :name => "NUMBER", :limit => NUMBER_MAX_PRECISION }, - :float => { :name => "NUMBER" }, + :integer => { :name => "NUMBER", :limit => 38 }, + :float => { :name => "BINARY_FLOAT" }, :decimal => { :name => "DECIMAL" }, :datetime => { :name => "DATE" }, # changed to native TIMESTAMP type @@ -462,7 +471,8 @@ def supports_transaction_isolation? #:nodoc: :date => { :name => "DATE" }, :binary => { :name => "BLOB" }, :boolean => { :name => "NUMBER", :limit => 1 }, - :raw => { :name => "RAW", :limit => 2000 } + :raw => { :name => "RAW", :limit => 2000 }, + :bigint => { :name => "NUMBER", :limit => 19 } } # if emulate_booleans_from_strings then store booleans in VARCHAR2 NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS = NATIVE_DATABASE_TYPES.dup.merge( @@ -667,22 +677,26 @@ def quote_timestamp_with_to_timestamp(value) #:nodoc: # Cast a +value+ to a type that the database understands. def type_cast(value, column) - case value - when true, false - if emulate_booleans_from_strings || column && column.type == :string - self.class.boolean_to_string(value) - else - value ? 1 : 0 - end - when Date, Time - if value.acts_like?(:time) - zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal - value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value + if column && column.cast_type.is_a?(Type::Serialized) + super + else + case value + when true, false + if emulate_booleans_from_strings || column && column.type == :string + self.class.boolean_to_string(value) + else + value ? 1 : 0 + end + when Date, Time + if value.acts_like?(:time) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value + else + value + end else - value + super end - else - super end end @@ -812,7 +826,7 @@ def write_lobs(table_name, klass, attributes, columns) #:nodoc: value = attributes[col.name] # changed sequence of next two lines - should check if value is nil before converting to yaml next if value.nil? || (value == '') - value = value.to_yaml if col.text? && klass.serialized_attributes[col.name] + value = value.to_yaml if col.cast_type.is_a?(Type::Serialized) # klass.serialized_attributes[col.name] uncached do sql = is_with_cpk ? "SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)} WHERE #{klass.composite_where_clause(id)} FOR UPDATE" : "SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)} WHERE #{quote_column_name(klass.primary_key)} = #{id} FOR UPDATE" @@ -832,17 +846,22 @@ def current_database # Current database session user def current_user - select_value("SELECT SYS_CONTEXT('userenv', 'session_user') FROM dual") + select_value("SELECT SYS_CONTEXT('userenv', 'current_schema') FROM dual") + end + + # Current database session schema + def current_schema + select_value("SELECT SYS_CONTEXT('userenv', 'current_schema') FROM dual") end # Default tablespace name of current user def default_tablespace - select_value("SELECT LOWER(default_tablespace) FROM user_users WHERE username = SYS_CONTEXT('userenv', 'session_user')") + select_value("SELECT LOWER(default_tablespace) FROM user_users WHERE username = SYS_CONTEXT('userenv', 'current_schema')") end def tables(name = nil) #:nodoc: select_values( - "SELECT DECODE(table_name, UPPER(table_name), LOWER(table_name), table_name) FROM all_tables WHERE owner = SYS_CONTEXT('userenv', 'session_user') AND secondary = 'N'", + "SELECT DECODE(table_name, UPPER(table_name), LOWER(table_name), table_name) FROM all_tables WHERE owner = SYS_CONTEXT('userenv', 'current_schema') AND secondary = 'N'", name) end @@ -855,7 +874,7 @@ def table_exists?(table_name) end def materialized_views #:nodoc: - select_values("SELECT LOWER(mview_name) FROM all_mviews WHERE owner = SYS_CONTEXT('userenv', 'session_user')") + select_values("SELECT LOWER(mview_name) FROM all_mviews WHERE owner = SYS_CONTEXT('userenv', 'current_schema')") end cattr_accessor :all_schema_indexes #:nodoc: @@ -906,7 +925,7 @@ def indexes(table_name, name = nil) #:nodoc: statement_parameters = $1 end end - all_schema_indexes << OracleEnhancedIndexDefinition.new(row['table_name'], row['index_name'], + all_schema_indexes << OracleEnhanced::IndexDefinition.new(row['table_name'], row['index_name'], row['uniqueness'] == "UNIQUE", row['index_type'] == 'DOMAIN' ? "#{row['ityp_owner']}.#{row['ityp_name']}" : nil, row['parameters'], statement_parameters, row['tablespace_name'] == default_tablespace_name ? nil : row['tablespace_name'], []) @@ -1038,7 +1057,7 @@ def columns_without_cache(table_name, name = nil) #:nodoc: end.map do |row| limit, scale = row['limit'], row['scale'] if limit || scale - row['sql_type'] += "(#{(limit || NUMBER_MAX_PRECISION).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")") + row['sql_type'] += "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")") end if row['sql_type_owner'] @@ -1055,19 +1074,61 @@ def columns_without_cache(table_name, name = nil) #:nodoc: # match newlines. row['data_default'].sub!(/^'(.*)'$/m, '\1') row['data_default'] = nil if row['data_default'] =~ /^(null|empty_[bc]lob\(\))$/i + # TODO: Needs better fix to fallback "N" to false + row['data_default'] = false if row['data_default'] == "N" end - OracleEnhancedColumn.new(oracle_downcase(row['name']), + # TODO: Consider to extract another method such as `get_cast_type` + case row['sql_type'] + when /decimal|numeric|number/i + if get_type_for_column(table_name, oracle_downcase(row['name'])) == :integer + cast_type = ActiveRecord::OracleEnhanced::Type::Integer.new + elsif OracleEnhancedAdapter.emulate_booleans && row['sql_type'].upcase == "NUMBER(1)" + cast_type = Type::Boolean.new + elsif OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(row['name'], table_name) + cast_type = ActiveRecord::OracleEnhanced::Type::Integer.new + else + cast_type = lookup_cast_type(row['sql_type']) + end + when /char/i + if get_type_for_column(table_name, oracle_downcase(row['name'])) == :string + cast_type = Type::String.new + elsif get_type_for_column(table_name, oracle_downcase(row['name'])) == :boolean + cast_type = Type::Boolean.new + elsif OracleEnhancedAdapter.emulate_booleans_from_strings && OracleEnhancedAdapter.is_boolean_column?(row['name'], row['sql_type'], table_name) + cast_type = Type::Boolean.new + else + cast_type = lookup_cast_type(row['sql_type']) + end + when /date/i + if get_type_for_column(table_name, oracle_downcase(row['name'])) == :date + cast_type = Type::Date.new + elsif get_type_for_column(table_name, oracle_downcase(row['name'])) == :datetime + cast_type = Type::DateTime.new + elsif OracleEnhancedAdapter.emulate_dates_by_column_name && OracleEnhancedAdapter.is_date_column?(row['name'], table_name) + cast_type = Type::Date.new + else + cast_type = lookup_cast_type(row['sql_type']) + end + else + cast_type = lookup_cast_type(row['sql_type']) + end + + new_column(oracle_downcase(row['name']), row['data_default'], + cast_type, row['sql_type'], row['nullable'] == 'Y', - # pass table name for table specific column definitions table_name, - # pass column type if specified in class definition - get_type_for_column(table_name, oracle_downcase(row['name'])), is_virtual) + is_virtual, + false ) end end + def new_column(name, default, cast_type, sql_type = nil, null = true, table_name = nil, virtual=false, returning_id=false) + OracleEnhancedColumn.new(name, default, cast_type, sql_type, null, table_name, virtual, returning_id) + end + # used just in tests to clear column cache def clear_columns_cache #:nodoc: @@columns_cache = nil @@ -1207,6 +1268,34 @@ def join_to_update(update, select) #:nodoc: protected + def initialize_type_map(m) + super + # oracle + register_class_with_limit m, %r(date)i, Type::DateTime + register_class_with_limit m, %r(raw)i, ActiveRecord::OracleEnhanced::Type::Raw + register_class_with_limit m, %r(timestamp)i, ActiveRecord::OracleEnhanced::Type::Timestamp + + m.register_type(%r(NUMBER)i) do |sql_type| + scale = extract_scale(sql_type) + precision = extract_precision(sql_type) + limit = extract_limit(sql_type) + if scale == 0 + ActiveRecord::OracleEnhanced::Type::Integer.new(precision: precision, limit: limit) + else + Type::Decimal.new(precision: precision, scale: scale) + end + end + end + + def extract_limit(sql_type) #:nodoc: + case sql_type + when /^bigint/i + 19 + when /\((.*)\)/ + $1.to_i + end + end + def translate_exception(exception, message) #:nodoc: case @connection.error_code(exception) when 1 @@ -1220,6 +1309,10 @@ def translate_exception(exception, message) #:nodoc: private + def select(sql, name = nil, binds = []) + exec_query(sql, name, binds) + end + def oracle_downcase(column_name) @connection.oracle_downcase(column_name) end @@ -1256,12 +1349,8 @@ def dbms_output_enabled? end protected - def log(sql, name, binds = nil) #:nodoc: - if binds - super sql, name, binds - else - super sql, name - end + def log(sql, name = "SQL", binds = [], statement_name = nil) #:nodoc: + super ensure log_dbms_output if dbms_output_enabled? end @@ -1289,38 +1378,47 @@ def log_dbms_output end # Implementation of standard schema definition statements and extensions for schema definition -require 'active_record/connection_adapters/oracle_enhanced_schema_statements' -require 'active_record/connection_adapters/oracle_enhanced_schema_statements_ext' +require 'active_record/connection_adapters/oracle_enhanced/schema_statements' +require 'active_record/connection_adapters/oracle_enhanced/schema_statements_ext' # Extensions for schema definition -require 'active_record/connection_adapters/oracle_enhanced_schema_definitions' +require 'active_record/connection_adapters/oracle_enhanced/schema_definitions' # Extensions for context index definition -require 'active_record/connection_adapters/oracle_enhanced_context_index' +require 'active_record/connection_adapters/oracle_enhanced/context_index' # Load additional methods for composite_primary_keys support -require 'active_record/connection_adapters/oracle_enhanced_cpk' +require 'active_record/connection_adapters/oracle_enhanced/cpk' # Load patch for dirty tracking methods -require 'active_record/connection_adapters/oracle_enhanced_dirty' +require 'active_record/connection_adapters/oracle_enhanced/dirty' # Patches and enhancements for schema dumper -require 'active_record/connection_adapters/oracle_enhanced_schema_dumper' +require 'active_record/connection_adapters/oracle_enhanced/schema_dumper' # Implementation of structure dump -require 'active_record/connection_adapters/oracle_enhanced_structure_dump' +require 'active_record/connection_adapters/oracle_enhanced/structure_dump' -require 'active_record/connection_adapters/oracle_enhanced_version' +require 'active_record/connection_adapters/oracle_enhanced/version' module ActiveRecord - autoload :OracleEnhancedProcedures, 'active_record/connection_adapters/oracle_enhanced_procedures' + autoload :OracleEnhancedProcedures, 'active_record/connection_adapters/oracle_enhanced/procedures' end # Patches and enhancements for column dumper -require 'active_record/connection_adapters/oracle_enhanced_column_dumper' +require 'active_record/connection_adapters/oracle_enhanced/column_dumper' # Moved SchemaCreation class -require 'active_record/connection_adapters/oracle_enhanced_schema_creation' +require 'active_record/connection_adapters/oracle_enhanced/schema_creation' # Moved DatabaseStetements -require 'active_record/connection_adapters/oracle_enhanced_database_statements' +require 'active_record/connection_adapters/oracle_enhanced/database_statements' + +# Add Type:Raw +require 'active_record/oracle_enhanced/type/raw' + +# Add Type:Timestamp +require 'active_record/oracle_enhanced/type/timestamp' + +# Add OracleEnhanced::Type::Integer +require 'active_record/oracle_enhanced/type/integer' diff --git a/lib/active_record/connection_adapters/oracle_enhanced_column_dumper.rb b/lib/active_record/connection_adapters/oracle_enhanced_column_dumper.rb deleted file mode 100644 index e54e96efd..000000000 --- a/lib/active_record/connection_adapters/oracle_enhanced_column_dumper.rb +++ /dev/null @@ -1,77 +0,0 @@ -module ActiveRecord #:nodoc: - module ConnectionAdapters #:nodoc: - module OracleEnhancedColumnDumper #:nodoc: - - def self.included(base) #:nodoc: - base.class_eval do - private - alias_method_chain :column_spec, :oracle_enhanced - alias_method_chain :prepare_column_options, :oracle_enhanced - alias_method_chain :migration_keys, :oracle_enhanced - - def oracle_enhanced_adapter? - # return original method if not using 'OracleEnhanced' - if (rails_env = defined?(Rails.env) ? Rails.env : (defined?(RAILS_ENV) ? RAILS_ENV : nil)) && - ActiveRecord::Base.configurations[rails_env] && - ActiveRecord::Base.configurations[rails_env]['adapter'] != 'oracle_enhanced' - return false - else - return true - end - end - end - end - - def column_spec_with_oracle_enhanced(column, types) - # return original method if not using 'OracleEnhanced' - return column_spec_without_oracle_enhanced(column, types) unless oracle_enhanced_adapter? - - spec = prepare_column_options(column, types) - (spec.keys - [:name, :type]).each do |k| - key_s = (k == :virtual_type ? "type: " : "#{k.to_s}: ") - spec[k] = key_s + spec[k] - end - spec - end - - def prepare_column_options_with_oracle_enhanced(column, types) - # return original method if not using 'OracleEnhanced' - return prepare_column_options_without_oracle_enhanced(column, types) unless oracle_enhanced_adapter? - - spec = {} - - spec[:name] = column.name.inspect - spec[:type] = column.virtual? ? 'virtual' : column.type.to_s - spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && column.type != :decimal - spec[:precision] = column.precision.inspect if !column.precision.nil? - spec[:scale] = column.scale.inspect if !column.scale.nil? - spec[:null] = 'false' if !column.null - spec[:as] = column.virtual_column_data_default if column.virtual? - spec[:default] = default_string(column.default) if column.has_default? && !column.virtual? - - if column.virtual? - # Supports backwards compatibility with older OracleEnhancedAdapter versions where 'NUMBER' virtual column type is not included in dump - if column.sql_type != "NUMBER" || ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.number_datatype_coercion != :decimal - spec[:virtual_type] = column.type.inspect - end - end - - spec - end - - def migration_keys_with_oracle_enhanced - # TODO `& column_specs.map(&:keys).flatten` should be exetuted here - # return original method if not using 'OracleEnhanced' - return migration_keys_without_oracle_enhanced unless oracle_enhanced_adapter? - - [:name, :limit, :precision, :scale, :default, :null, :as, :virtual_type] - end - - - end - end -end - -ActiveRecord::ConnectionAdapters::ColumnDumper.class_eval do - include ActiveRecord::ConnectionAdapters::OracleEnhancedColumnDumper -end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb b/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb deleted file mode 100644 index dcc91597c..000000000 --- a/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +++ /dev/null @@ -1,350 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module OracleEnhancedContextIndex - - # Define full text index with Oracle specific CONTEXT index type - # - # Oracle CONTEXT index by default supports full text indexing of one column. - # This method allows full text index creation also on several columns - # as well as indexing related table columns by generating stored procedure - # that concatenates all columns for indexing as well as generating trigger - # that will update main index column to trigger reindexing of record. - # - # Use +contains+ ActiveRecord model instance method to add CONTAINS where condition - # and order by score of matched results. - # - # Options: - # - # * :name - # * :index_column - # * :index_column_trigger_on - # * :tablespace - # * :sync - 'MANUAL', 'EVERY "interval-string"' or 'ON COMMIT' (defaults to 'MANUAL'). - # * :lexer - Lexer options (e.g. :type => 'BASIC_LEXER', :base_letter => true). - # * :wordlist - Wordlist options (e.g. :type => 'BASIC_WORDLIST', :prefix_index => true). - # * :transactional - When +true+, the CONTAINS operator will process inserted and updated rows. - # - # ===== Examples - # - # ====== Creating single column index - # add_context_index :posts, :title - # search with - # Post.contains(:title, 'word') - # - # ====== Creating index on several columns - # add_context_index :posts, [:title, :body] - # search with (use first column as argument for contains method but it will search in all index columns) - # Post.contains(:title, 'word') - # - # ====== Creating index on several columns with dummy index column and commit option - # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT' - # search with - # Post.contains(:all_text, 'word') - # - # ====== Creating index with trigger option (will reindex when specified columns are updated) - # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT', - # :index_column_trigger_on => [:created_at, :updated_at] - # search with - # Post.contains(:all_text, 'word') - # - # ====== Creating index on multiple tables - # add_context_index :posts, - # [:title, :body, - # # specify aliases always with AS keyword - # "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id" - # ], - # :name => 'post_and_comments_index', - # :index_column => :all_text, :index_column_trigger_on => [:updated_at, :comments_count], - # :sync => 'ON COMMIT' - # search in any table columns - # Post.contains(:all_text, 'word') - # search in specified column - # Post.contains(:all_text, "aaa within title") - # Post.contains(:all_text, "bbb within comment_author") - # - # ====== Creating index using lexer - # add_context_index :posts, :title, :lexer => { :type => 'BASIC_LEXER', :base_letter => true, ... } - # - # ====== Creating index using wordlist - # add_context_index :posts, :title, :wordlist => { :type => 'BASIC_WORDLIST', :prefix_index => true, ... } - # - # ====== Creating transactional index (will reindex changed rows when querying) - # add_context_index :posts, :title, :transactional => true - # - def add_context_index(table_name, column_name, options = {}) - self.all_schema_indexes = nil - column_names = Array(column_name) - index_name = options[:name] || index_name(table_name, :column => options[:index_column] || column_names, - # CONEXT index name max length is 25 - :identifier_max_length => 25) - - quoted_column_name = quote_column_name(options[:index_column] || column_names.first) - if options[:index_column_trigger_on] - raise ArgumentError, "Option :index_column should be specified together with :index_column_trigger_on option" \ - unless options[:index_column] - create_index_column_trigger(table_name, index_name, options[:index_column], options[:index_column_trigger_on]) - end - - sql = "CREATE INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" - sql << " (#{quoted_column_name})" - sql << " INDEXTYPE IS CTXSYS.CONTEXT" - parameters = [] - if column_names.size > 1 - procedure_name = default_datastore_procedure(index_name) - datastore_name = default_datastore_name(index_name) - create_datastore_procedure(table_name, procedure_name, column_names, options) - create_datastore_preference(datastore_name, procedure_name) - parameters << "DATASTORE #{datastore_name} SECTION GROUP CTXSYS.AUTO_SECTION_GROUP" - end - if options[:tablespace] - storage_name = default_storage_name(index_name) - create_storage_preference(storage_name, options[:tablespace]) - parameters << "STORAGE #{storage_name}" - end - if options[:sync] - parameters << "SYNC(#{options[:sync]})" - end - if options[:lexer] && (lexer_type = options[:lexer][:type]) - lexer_name = default_lexer_name(index_name) - (lexer_options = options[:lexer].dup).delete(:type) - create_lexer_preference(lexer_name, lexer_type, lexer_options) - parameters << "LEXER #{lexer_name}" - end - if options[:wordlist] && (wordlist_type = options[:wordlist][:type]) - wordlist_name = default_wordlist_name(index_name) - (wordlist_options = options[:wordlist].dup).delete(:type) - create_wordlist_preference(wordlist_name, wordlist_type, wordlist_options) - parameters << "WORDLIST #{wordlist_name}" - end - if options[:transactional] - parameters << "TRANSACTIONAL" - end - unless parameters.empty? - sql << " PARAMETERS ('#{parameters.join(' ')}')" - end - execute sql - end - - # Drop full text index with Oracle specific CONTEXT index type - def remove_context_index(table_name, options = {}) - self.all_schema_indexes = nil - unless Hash === options # if column names passed as argument - options = {:column => Array(options)} - end - index_name = options[:name] || index_name(table_name, - :column => options[:index_column] || options[:column], :identifier_max_length => 25) - execute "DROP INDEX #{index_name}" - drop_ctx_preference(default_datastore_name(index_name)) - drop_ctx_preference(default_storage_name(index_name)) - procedure_name = default_datastore_procedure(index_name) - execute "DROP PROCEDURE #{quote_table_name(procedure_name)}" rescue nil - drop_index_column_trigger(index_name) - end - - private - - def create_datastore_procedure(table_name, procedure_name, column_names, options) - quoted_table_name = quote_table_name(table_name) - select_queries, column_names = column_names.partition { |c| c.to_s =~ /^\s*SELECT\s+/i } - select_queries = select_queries.map { |s| s.strip.gsub(/\s+/, ' ') } - keys, selected_columns = parse_select_queries(select_queries) - quoted_column_names = (column_names+keys).map{|col| quote_column_name(col)} - execute compress_lines(<<-SQL) - CREATE OR REPLACE PROCEDURE #{quote_table_name(procedure_name)} - (p_rowid IN ROWID, - p_clob IN OUT NOCOPY CLOB) IS - -- add_context_index_parameters #{(column_names+select_queries).inspect}#{!options.empty? ? ', ' << options.inspect[1..-2] : ''} - #{ - selected_columns.map do |cols| - cols.map do |col| - raise ArgumentError, "Alias #{col} too large, should be 28 or less characters long" unless col.length <= 28 - "l_#{col} VARCHAR2(32767);\n" - end.join - end.join - } BEGIN - FOR r1 IN ( - SELECT #{quoted_column_names.join(', ')} - FROM #{quoted_table_name} - WHERE #{quoted_table_name}.ROWID = p_rowid - ) LOOP - #{ - (column_names.map do |col| - col = col.to_s - "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" << - "IF LENGTH(r1.#{col}) > 0 THEN\n" << - "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(r1.#{col}), r1.#{col});\n" << - "END IF;\n" << - "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '');\n" - end.join) << - (selected_columns.zip(select_queries).map do |cols, query| - (cols.map do |col| - "l_#{col} := '';\n" - end.join) << - "FOR r2 IN (\n" << - query.gsub(/:(\w+)/,"r1.\\1") << "\n) LOOP\n" << - (cols.map do |col| - "l_#{col} := l_#{col} || r2.#{col} || CHR(10);\n" - end.join) << - "END LOOP;\n" << - (cols.map do |col| - col = col.to_s - "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" << - "IF LENGTH(l_#{col}) > 0 THEN\n" << - "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(l_#{col}), l_#{col});\n" << - "END IF;\n" << - "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '');\n" - end.join) - end.join) - } - END LOOP; - END; - SQL - end - - def parse_select_queries(select_queries) - keys = [] - selected_columns = [] - select_queries.each do |query| - # get primary or foreign keys like :id or :something_id - keys << (query.scan(/:\w+/).map{|k| k[1..-1].downcase.to_sym}) - select_part = query.scan(/^select\s.*\sfrom/i).first - selected_columns << select_part.scan(/\sas\s+(\w+)/i).map{|c| c.first} - end - [keys.flatten.uniq, selected_columns] - end - - def create_datastore_preference(datastore_name, procedure_name) - drop_ctx_preference(datastore_name) - execute <<-SQL - BEGIN - CTX_DDL.CREATE_PREFERENCE('#{datastore_name}', 'USER_DATASTORE'); - CTX_DDL.SET_ATTRIBUTE('#{datastore_name}', 'PROCEDURE', '#{procedure_name}'); - END; - SQL - end - - def create_storage_preference(storage_name, tablespace) - drop_ctx_preference(storage_name) - sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{storage_name}', 'BASIC_STORAGE');\n" - ['I_TABLE_CLAUSE', 'K_TABLE_CLAUSE', 'R_TABLE_CLAUSE', - 'N_TABLE_CLAUSE', 'I_INDEX_CLAUSE', 'P_TABLE_CLAUSE'].each do |clause| - default_clause = case clause - when 'R_TABLE_CLAUSE'; 'LOB(DATA) STORE AS (CACHE) ' - when 'I_INDEX_CLAUSE'; 'COMPRESS 2 ' - else '' - end - sql << "CTX_DDL.SET_ATTRIBUTE('#{storage_name}', '#{clause}', '#{default_clause}TABLESPACE #{tablespace}');\n" - end - sql << "END;\n" - execute sql - end - - def create_lexer_preference(lexer_name, lexer_type, options) - drop_ctx_preference(lexer_name) - sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{lexer_name}', '#{lexer_type}');\n" - options.each do |key, value| - plsql_value = case value - when String; "'#{value}'" - when true; "'YES'" - when false; "'NO'" - when nil; 'NULL' - else value - end - sql << "CTX_DDL.SET_ATTRIBUTE('#{lexer_name}', '#{key}', #{plsql_value});\n" - end - sql << "END;\n" - execute sql - end - - def create_wordlist_preference(wordlist_name, wordlist_type, options) - drop_ctx_preference(wordlist_name) - sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{wordlist_name}', '#{wordlist_type}');\n" - options.each do |key, value| - plsql_value = case value - when String; "'#{value}'" - when true; "'YES'" - when false; "'NO'" - when nil; 'NULL' - else value - end - sql << "CTX_DDL.SET_ATTRIBUTE('#{wordlist_name}', '#{key}', #{plsql_value});\n" - end - sql << "END;\n" - execute sql - end - - def drop_ctx_preference(preference_name) - execute "BEGIN CTX_DDL.DROP_PREFERENCE('#{preference_name}'); END;" rescue nil - end - - def create_index_column_trigger(table_name, index_name, index_column, index_column_source) - trigger_name = default_index_column_trigger_name(index_name) - columns = Array(index_column_source) - quoted_column_names = columns.map{|col| quote_column_name(col)}.join(', ') - execute compress_lines(<<-SQL) - CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)} - BEFORE UPDATE OF #{quoted_column_names} ON #{quote_table_name(table_name)} FOR EACH ROW - BEGIN - :new.#{quote_column_name(index_column)} := '1'; - END; - SQL - end - - def drop_index_column_trigger(index_name) - trigger_name = default_index_column_trigger_name(index_name) - execute "DROP TRIGGER #{quote_table_name(trigger_name)}" rescue nil - end - - def default_datastore_procedure(index_name) - "#{index_name}_prc" - end - - def default_datastore_name(index_name) - "#{index_name}_dst" - end - - def default_storage_name(index_name) - "#{index_name}_sto" - end - - def default_index_column_trigger_name(index_name) - "#{index_name}_trg" - end - - def default_lexer_name(index_name) - "#{index_name}_lex" - end - - def default_wordlist_name(index_name) - "#{index_name}_wl" - end - - module BaseClassMethods - # Declare that model table has context index defined. - # As a result contains class scope method is defined. - def has_context_index - extend ContextIndexClassMethods - end - end - - module ContextIndexClassMethods - # Add context index condition. - def contains(column, query, options ={}) - score_label = options[:label].to_i || 1 - where("CONTAINS(#{connection.quote_column_name(column)}, ?, #{score_label}) > 0", query). - order("SCORE(#{score_label}) DESC") - end - end - - end - - end -end - -ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do - include ActiveRecord::ConnectionAdapters::OracleEnhancedContextIndex -end - -ActiveRecord::Base.class_eval do - extend ActiveRecord::ConnectionAdapters::OracleEnhancedContextIndex::BaseClassMethods -end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_database_statements.rb b/lib/active_record/connection_adapters/oracle_enhanced_database_statements.rb deleted file mode 100644 index 44a75c809..000000000 --- a/lib/active_record/connection_adapters/oracle_enhanced_database_statements.rb +++ /dev/null @@ -1,262 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module OracleEnhancedDatabaseStatements - # DATABASE STATEMENTS ====================================== - # - # see: abstract/database_statements.rb - - # Executes a SQL statement - def execute(sql, name = nil) - log(sql, name) { @connection.exec(sql) } - end - - def substitute_at(column, index) - Arel::Nodes::BindParam.new (":a#{index + 1}") - end - - def clear_cache! - @statements.clear - end - - def exec_query(sql, name = 'SQL', binds = []) - type_casted_binds = binds.map { |col, val| - [col, type_cast(val, col)] - } - log(sql, name, type_casted_binds) do - cursor = nil - cached = false - if without_prepared_statement?(binds) - cursor = @connection.prepare(sql) - else - unless @statements.key? sql - @statements[sql] = @connection.prepare(sql) - end - - cursor = @statements[sql] - - binds.each_with_index do |bind, i| - col, val = bind - cursor.bind_param(i + 1, type_cast(val, col), col) - end - - cached = true - end - - cursor.exec - - if name == 'EXPLAIN' and sql =~ /^EXPLAIN/ - res = true - else - columns = cursor.get_col_names.map do |col_name| - @connection.oracle_downcase(col_name) - end - rows = [] - fetch_options = {:get_lob_value => (name != 'Writable Large Object')} - while row = cursor.fetch(fetch_options) - rows << row - end - res = ActiveRecord::Result.new(columns, rows) - end - - cursor.close unless cached - res - end - end - - def supports_statement_cache? - true - end - - def supports_explain? - true - end - - def explain(arel, binds = []) - sql = "EXPLAIN PLAN FOR #{to_sql(arel, binds)}" - return if sql =~ /FROM all_/ - if ORACLE_ENHANCED_CONNECTION == :jdbc - exec_query(sql, 'EXPLAIN', binds) - else - exec_query(sql, 'EXPLAIN') - end - select_values("SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY)", 'EXPLAIN').join("\n") - end - - # Returns an array of arrays containing the field values. - # Order is the same as that returned by #columns. - def select_rows(sql, name = nil, binds = []) - exec_query(sql, name, binds).rows - end - - # Executes an INSERT statement and returns the new record's ID - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - # if primary key value is already prefetched from sequence - # or if there is no primary key - if id_value || pk.nil? - execute(sql, name) - return id_value - end - - sql_with_returning = sql + @connection.returning_clause(quote_column_name(pk)) - log(sql, name) do - @connection.exec_with_returning(sql_with_returning) - end - end - protected :insert_sql - - # New method in ActiveRecord 3.1 - # Will add RETURNING clause in case of trigger generated primary keys - def sql_for_insert(sql, pk, id_value, sequence_name, binds) - unless id_value || pk.nil? || (defined?(CompositePrimaryKeys) && pk.kind_of?(CompositePrimaryKeys::CompositeKeys)) - sql = "#{sql} RETURNING #{quote_column_name(pk)} INTO :returning_id" - returning_id_col = OracleEnhancedColumn.new("returning_id", nil, "number", true, "dual", :integer, true, true) - (binds = binds.dup) << [returning_id_col, nil] - end - [sql, binds] - end - - # New method in ActiveRecord 3.1 - def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) - type_casted_binds = binds.map { |col, val| - [col, type_cast(val, col)] - } - log(sql, name, type_casted_binds) do - returning_id_col = returning_id_index = nil - if without_prepared_statement?(binds) - cursor = @connection.prepare(sql) - else - unless @statements.key? (sql) - @statements[sql] = @connection.prepare(sql) - end - - cursor = @statements[sql] - - binds.each_with_index do |bind, i| - col, val = bind - if col.returning_id? - returning_id_col = [col] - returning_id_index = i + 1 - cursor.bind_returning_param(returning_id_index, Integer) - else - cursor.bind_param(i + 1, type_cast(val, col), col) - end - end - end - - cursor.exec_update - - rows = [] - if returning_id_index - returning_id = cursor.get_returning_param(returning_id_index, Integer) - rows << [returning_id] - end - ActiveRecord::Result.new(returning_id_col || [], rows) - end - end - - # New method in ActiveRecord 3.1 - def exec_update(sql, name, binds) - log(sql, name, binds) do - cached = false - if without_prepared_statement?(binds) - cursor = @connection.prepare(sql) - else - cursor = if @statements.key?(sql) - @statements[sql] - else - @statements[sql] = @connection.prepare(sql) - end - - binds.each_with_index do |bind, i| - col, val = bind - cursor.bind_param(i + 1, type_cast(val, col), col) - end - cached = true - end - - res = cursor.exec_update - cursor.close unless cached - res - end - end - - alias :exec_delete :exec_update - - def begin_db_transaction #:nodoc: - @connection.autocommit = false - end - - def transaction_isolation_levels - # Oracle database supports `READ COMMITTED` and `SERIALIZABLE` - # No read uncommitted nor repeatable read supppoted - # http://docs.oracle.com/cd/E11882_01/server.112/e26088/statements_10005.htm#SQLRF55422 - { - read_committed: "READ COMMITTED", - serializable: "SERIALIZABLE" - } - end - - def begin_isolated_db_transaction(isolation) - begin_db_transaction - execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}" - end - - def commit_db_transaction #:nodoc: - @connection.commit - ensure - @connection.autocommit = true - end - - def rollback_db_transaction #:nodoc: - @connection.rollback - ensure - @connection.autocommit = true - end - - def create_savepoint(name = current_savepoint_name) #:nodoc: - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint(name = current_savepoint_name) #:nodoc: - execute("ROLLBACK TO #{current_savepoint_name}") - end - - def release_savepoint(name = current_savepoint_name) #:nodoc: - # there is no RELEASE SAVEPOINT statement in Oracle - end - - # Returns default sequence name for table. - # Will take all or first 26 characters of table name and append _seq suffix - def default_sequence_name(table_name, primary_key = nil) - table_name.to_s.gsub /(^|\.)([\w$-]{1,#{sequence_name_length-4}})([\w$-]*)$/, '\1\2_seq' - end - - # Inserts the given fixture into the table. Overridden to properly handle lobs. - def insert_fixture(fixture, table_name) #:nodoc: - super - - if ActiveRecord::Base.pluralize_table_names - klass = table_name.to_s.singularize.camelize - else - klass = table_name.to_s.camelize - end - - klass = klass.constantize rescue nil - if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base) - write_lobs(table_name, klass, fixture, klass.lob_columns) - end - end - - private - - def select(sql, name = nil, binds = []) - exec_query(sql, name, binds) - end - - end - end -end - -ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do - include ActiveRecord::ConnectionAdapters::OracleEnhancedDatabaseStatements -end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb b/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb deleted file mode 100644 index e91fb4c9e..000000000 --- a/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +++ /dev/null @@ -1,45 +0,0 @@ -module ActiveRecord #:nodoc: - module ConnectionAdapters #:nodoc: - module OracleEnhancedDirty #:nodoc: - - module InstanceMethods #:nodoc: - private - - def _field_changed?(attr, old, value) - if column = column_for_attribute(attr) - # Added also :decimal type - if ([:integer, :decimal, :float].include? column.type) && column.null && (old.nil? || old == 0) && value.blank? - # For nullable integer/decimal/float columns, NULL gets stored in database for blank (i.e. '') values. - # Hence we don't record it as a change if the value changes from nil to ''. - # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll - # be typecast back to 0 (''.to_i => 0) - value = nil - elsif column.type == :string && column.null && old.nil? - # Oracle stores empty string '' as NULL - # therefore need to convert empty string value to nil if old value is nil - value = nil if value == '' - elsif old == 0 && value.is_a?(String) && value.present? && non_zero?(value) - value = nil - else - value = column.type_cast(value) - end - end - - old != value - end - - def non_zero?(value) - value !~ /\A0+(\.0+)?\z/ - end - - end - - end - end -end - -if ActiveRecord::Base.method_defined?(:changed?) - ActiveRecord::Base.class_eval do - include ActiveRecord::ConnectionAdapters::OracleEnhancedDirty::InstanceMethods - end -end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb b/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb deleted file mode 100644 index 489d2964c..000000000 --- a/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +++ /dev/null @@ -1,197 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - class OracleEnhancedForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: - end - - class OracleEnhancedSynonymDefinition < Struct.new(:name, :table_owner, :table_name, :db_link) #:nodoc: - end - - class OracleEnhancedIndexDefinition < Struct.new(:table, :name, :unique, :type, :parameters, :statement_parameters, - :tablespace, :columns) #:nodoc: - end - - module OracleEnhancedSchemaDefinitions #:nodoc: - def self.included(base) - base::TableDefinition.class_eval do - include OracleEnhancedTableDefinition - end - - # Available starting from ActiveRecord 2.1 - base::Table.class_eval do - include OracleEnhancedTable - end if defined?(base::Table) - end - end - - module OracleEnhancedTableDefinition - class ForeignKey < Struct.new(:base, :to_table, :options) #:nodoc: - def to_sql - base.foreign_key_definition(to_table, options) - end - alias to_s :to_sql - end - - def self.included(base) #:nodoc: - base.class_eval do - alias_method_chain :references, :foreign_keys - alias_method_chain :column, :virtual_columns - end - end - - def raw(name, options={}) - column(name, :raw, options) - end - - def virtual(* args) - options = args.extract_options! - column_names = args - column_names.each { |name| column(name, :virtual, options) } - end - - def column_with_virtual_columns(name, type, options = {}) - if type == :virtual - default = {:type => options[:type]} - if options[:as] - default[:as] = options[:as] - elsif options[:default] - warn "[DEPRECATION] virtual column `:default` option is deprecated. Please use `:as` instead." - default[:as] = options[:default] - else - raise "No virtual column definition found." - end - options[:default] = default - end - column_without_virtual_columns(name, type, options) - end - - # Adds a :foreign_key option to TableDefinition.references. - # If :foreign_key is true, a foreign key constraint is added to the table. - # You can also specify a hash, which is passed as foreign key options. - # - # ===== Examples - # ====== Add goat_id column and a foreign key to the goats table. - # t.references(:goat, :foreign_key => true) - # ====== Add goat_id column and a cascading foreign key to the goats table. - # t.references(:goat, :foreign_key => {:dependent => :delete}) - # - # Note: No foreign key is created if :polymorphic => true is used. - # Note: If no name is specified, the database driver creates one for you! - def references_with_foreign_keys(*args) - options = args.extract_options! - index_options = options[:index] - fk_options = options.delete(:foreign_key) - - if fk_options && !options[:polymorphic] - fk_options = {} if fk_options == true - args.each do |to_table| - foreign_key(to_table, fk_options) - add_index(to_table, "#{to_table}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options - end - end - - references_without_foreign_keys(*(args << options)) - end - - # Defines a foreign key for the table. +to_table+ can be a single Symbol, or - # an Array of Symbols. See SchemaStatements#add_foreign_key - # - # ===== Examples - # ====== Creating a simple foreign key - # t.foreign_key(:people) - # ====== Defining the column - # t.foreign_key(:people, :column => :sender_id) - # ====== Creating a named foreign key - # t.foreign_key(:people, :column => :sender_id, :name => 'sender_foreign_key') - # ====== Defining the column of the +to_table+. - # t.foreign_key(:people, :column => :sender_id, :primary_key => :person_id) - def foreign_key(to_table, options = {}) - #TODO - if ActiveRecord::Base.connection.supports_foreign_keys? - to_table = to_table.to_s.pluralize if ActiveRecord::Base.pluralize_table_names - foreign_keys << ForeignKey.new(@base, to_table, options) - else - raise ArgumentError, "this ActiveRecord adapter is not supporting foreign_key definition" - end - end - - def foreign_keys - @foreign_keys ||= [] - end - end - - module OracleEnhancedTable - def self.included(base) #:nodoc: - base.class_eval do - alias_method_chain :references, :foreign_keys - end - end - - # Adds a new foreign key to the table. +to_table+ can be a single Symbol, or - # an Array of Symbols. See SchemaStatements#add_foreign_key - # - # ===== Examples - # ====== Creating a simple foreign key - # t.foreign_key(:people) - # ====== Defining the column - # t.foreign_key(:people, :column => :sender_id) - # ====== Creating a named foreign key - # t.foreign_key(:people, :column => :sender_id, :name => 'sender_foreign_key') - # ====== Defining the column of the +to_table+. - # t.foreign_key(:people, :column => :sender_id, :primary_key => :person_id) - def foreign_key(to_table, options = {}) - if @base.respond_to?(:supports_foreign_keys?) && @base.supports_foreign_keys? - to_table = to_table.to_s.pluralize if ActiveRecord::Base.pluralize_table_names - @base.add_foreign_key(@table_name, to_table, options) - else - raise ArgumentError, "this ActiveRecord adapter is not supporting foreign_key definition" - end - end - - # Remove the given foreign key from the table. - # - # ===== Examples - # ====== Remove the suppliers_company_id_fk in the suppliers table. - # t.remove_foreign_key :companies - # ====== Remove the foreign key named accounts_branch_id_fk in the accounts table. - # remove_foreign_key :column => :branch_id - # ====== Remove the foreign key named party_foreign_key in the accounts table. - # remove_index :name => :party_foreign_key - def remove_foreign_key(options = {}) - @base.remove_foreign_key(@table_name, options) - end - - # Adds a :foreign_key option to TableDefinition.references. - # If :foreign_key is true, a foreign key constraint is added to the table. - # You can also specify a hash, which is passed as foreign key options. - # - # ===== Examples - # ====== Add goat_id column and a foreign key to the goats table. - # t.references(:goat, :foreign_key => true) - # ====== Add goat_id column and a cascading foreign key to the goats table. - # t.references(:goat, :foreign_key => {:dependent => :delete}) - # - # Note: No foreign key is created if :polymorphic => true is used. - def references_with_foreign_keys(*args) - options = args.extract_options! - polymorphic = options[:polymorphic] - index_options = options[:index] - fk_options = options.delete(:foreign_key) - - references_without_foreign_keys(*(args << options)) - # references_without_foreign_keys adds {:type => :integer} - args.extract_options! - if fk_options && !polymorphic - fk_options = {} if fk_options == true - args.each do |to_table| - foreign_key(to_table, fk_options) - add_index(to_table, "#{to_table}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options - end - end - end - end - end -end - -ActiveRecord::ConnectionAdapters.class_eval do - include ActiveRecord::ConnectionAdapters::OracleEnhancedSchemaDefinitions -end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb b/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb deleted file mode 100644 index 1086e3615..000000000 --- a/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +++ /dev/null @@ -1,450 +0,0 @@ -# -*- coding: utf-8 -*- -require 'digest/sha1' - -module ActiveRecord - module ConnectionAdapters - module OracleEnhancedSchemaStatements - # SCHEMA STATEMENTS ======================================== - # - # see: abstract/schema_statements.rb - - # Additional options for +create_table+ method in migration files. - # - # You can specify individual starting value in table creation migration file, e.g.: - # - # create_table :users, :sequence_start_value => 100 do |t| - # # ... - # end - # - # You can also specify other sequence definition additional parameters, e.g.: - # - # create_table :users, :sequence_start_value => “100 NOCACHE INCREMENT BY 10” do |t| - # # ... - # end - # - # Create primary key trigger (so that you can skip primary key value in INSERT statement). - # By default trigger name will be "table_name_pkt", you can override the name with - # :trigger_name option (but it is not recommended to override it as then this trigger will - # not be detected by ActiveRecord model and it will still do prefetching of sequence value). - # Example: - # - # create_table :users, :primary_key_trigger => true do |t| - # # ... - # end - # - # It is possible to add table and column comments in table creation migration files: - # - # create_table :employees, :comment => “Employees and contractors” do |t| - # t.string :first_name, :comment => “Given name” - # t.string :last_name, :comment => “Surname” - # end - - def create_table(name, options = {}) - create_sequence = options[:id] != false - column_comments = {} - temporary = options.delete(:temporary) - additional_options = options - table_definition = create_table_definition name, temporary, additional_options - table_definition.primary_key(options[:primary_key] || Base.get_primary_key(name.to_s.singularize)) unless options[:id] == false - - # store that primary key was defined in create_table block - unless create_sequence - class << table_definition - attr_accessor :create_sequence - def primary_key(*args) - self.create_sequence = true - super(*args) - end - end - end - - # store column comments - class << table_definition - attr_accessor :column_comments - def column(name, type, options = {}) - if options[:comment] - self.column_comments ||= {} - self.column_comments[name] = options[:comment] - end - super(name, type, options) - end - end - - yield table_definition if block_given? - create_sequence = create_sequence || table_definition.create_sequence - column_comments = table_definition.column_comments if table_definition.column_comments - tablespace = tablespace_for(:table, options[:tablespace]) - - if options[:force] && table_exists?(name) - drop_table(name, options) - end - - execute schema_creation.accept table_definition - - create_sequence_and_trigger(name, options) if create_sequence - - add_table_comment name, options[:comment] - column_comments.each do |column_name, comment| - add_comment name, column_name, comment - end - table_definition.indexes.each_pair { |c,o| add_index name, c, o } - - unless table_definition.foreign_keys.nil? - table_definition.foreign_keys.each do |foreign_key| - add_foreign_key(table_definition.name, foreign_key.to_table, foreign_key.options) - end - end - end - - def create_table_definition(name, temporary, options) - TableDefinition.new native_database_types, name, temporary, options - end - - def rename_table(table_name, new_name) #:nodoc: - if new_name.to_s.length > table_name_length - raise ArgumentError, "New table name '#{new_name}' is too long; the limit is #{table_name_length} characters" - end - if "#{new_name}_seq".to_s.length > sequence_name_length - raise ArgumentError, "New sequence name '#{new_name}_seq' is too long; the limit is #{sequence_name_length} characters" - end - execute "RENAME #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" - execute "RENAME #{quote_table_name("#{table_name}_seq")} TO #{quote_table_name("#{new_name}_seq")}" rescue nil - - rename_table_indexes(table_name, new_name) - end - - def drop_table(name, options = {}) #:nodoc: - super(name) - seq_name = options[:sequence_name] || default_sequence_name(name) - execute "DROP SEQUENCE #{quote_table_name(seq_name)}" rescue nil - rescue ActiveRecord::StatementInvalid => e - raise e unless options[:if_exists] - ensure - clear_table_columns_cache(name) - self.all_schema_indexes = nil - end - - def initialize_schema_migrations_table - sm_table = ActiveRecord::Migrator.schema_migrations_table_name - - unless table_exists?(sm_table) - index_name = "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" - if index_name.length > index_name_length - truncate_to = index_name_length - index_name.to_s.length - 1 - truncated_name = "unique_schema_migrations"[0..truncate_to] - index_name = "#{Base.table_name_prefix}#{truncated_name}#{Base.table_name_suffix}" - end - - create_table(sm_table, :id => false) do |schema_migrations_table| - schema_migrations_table.column :version, :string, :null => false - end - add_index sm_table, :version, :unique => true, :name => index_name - - # Backwards-compatibility: if we find schema_info, assume we've - # migrated up to that point: - si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix - if table_exists?(si_table) - ActiveSupport::Deprecation.warn "Usage of the schema table `#{si_table}` is deprecated. Please switch to using `schema_migrations` table" - - old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i - assume_migrated_upto_version(old_version) - drop_table(si_table) - end - end - end - - # clear cached indexes when adding new index - def add_index(table_name, column_name, options = {}) #:nodoc: - column_names = Array(column_name) - index_name = index_name(table_name, column: column_names) - - if Hash === options # legacy support, since this param was a string - options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :tablespace, :options, :using) - - index_type = options[:unique] ? "UNIQUE" : "" - index_name = options[:name].to_s if options.key?(:name) - tablespace = tablespace_for(:index, options[:tablespace]) - max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length - additional_options = options[:options] - else - if options - message = "Passing a string as third argument of `add_index` is deprecated and will" + - " be removed in Rails 4.1." + - " Use add_index(#{table_name.inspect}, #{column_name.inspect}, unique: true) instead" - - ActiveSupport::Deprecation.warn message - end - - index_type = options - additional_options = nil - max_index_length = allowed_index_name_length - end - - if index_name.to_s.length > max_index_length - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" - end - if index_name_exists?(table_name, index_name, false) - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" - end - quoted_column_names = column_names.map { |e| quote_column_name_or_expression(e) }.join(", ") - - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})#{tablespace} #{additional_options}" - ensure - self.all_schema_indexes = nil - end - - # Remove the given index from the table. - # Gives warning if index does not exist - def remove_index(table_name, options = {}) #:nodoc: - index_name = index_name(table_name, options) - unless index_name_exists?(table_name, index_name, true) - # sometimes options can be String or Array with column names - options = {} unless options.is_a?(Hash) - if options.has_key? :name - options_without_column = options.dup - options_without_column.delete :column - index_name_without_column = index_name(table_name, options_without_column) - return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) - end - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" - end - remove_index!(table_name, index_name) - end - - # clear cached indexes when removing index - def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_column_name(index_name)}" - ensure - self.all_schema_indexes = nil - end - - # returned shortened index name if default is too large - def index_name(table_name, options) #:nodoc: - default_name = super(table_name, options).to_s - # sometimes options can be String or Array with column names - options = {} unless options.is_a?(Hash) - identifier_max_length = options[:identifier_max_length] || index_name_length - return default_name if default_name.length <= identifier_max_length - - # remove 'index', 'on' and 'and' keywords - shortened_name = "i_#{table_name}_#{Array(options[:column]) * '_'}" - - # leave just first three letters from each word - if shortened_name.length > identifier_max_length - shortened_name = shortened_name.split('_').map{|w| w[0,3]}.join('_') - end - # generate unique name using hash function - if shortened_name.length > identifier_max_length - shortened_name = 'i'+Digest::SHA1.hexdigest(default_name)[0,identifier_max_length-1] - end - @logger.warn "#{adapter_name} shortened default index name #{default_name} to #{shortened_name}" if @logger - shortened_name - end - - # Verify the existence of an index with a given name. - # - # The default argument is returned if the underlying implementation does not define the indexes method, - # as there's no way to determine the correct answer in that case. - # - # Will always query database and not index cache. - def index_name_exists?(table_name, index_name, default) - (owner, table_name, db_link) = @connection.describe(table_name) - result = select_value(<<-SQL) - SELECT 1 FROM all_indexes#{db_link} i - WHERE i.owner = '#{owner}' - AND i.table_owner = '#{owner}' - AND i.table_name = '#{table_name}' - AND i.index_name = '#{index_name.to_s.upcase}' - SQL - result == 1 - end - - def rename_index(table_name, index_name, new_index_name) #:nodoc: - unless index_name_exists?(table_name, index_name, true) - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" - end - execute "ALTER INDEX #{quote_column_name(index_name)} rename to #{quote_column_name(new_index_name)}" - ensure - self.all_schema_indexes = nil - end - - def add_column(table_name, column_name, type, options = {}) #:nodoc: - if type.to_sym == :virtual - type = options[:type] - end - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} " - add_column_sql << type_to_sql(type, options[:limit], options[:precision], options[:scale]) if type - - add_column_options!(add_column_sql, options.merge(:type=>type, :column_name=>column_name, :table_name=>table_name)) - - add_column_sql << tablespace_for((type_to_sql(type).downcase.to_sym), nil, table_name, column_name) if type - - execute(add_column_sql) - - create_sequence_and_trigger(table_name, options) if type && type.to_sym == :primary_key - ensure - clear_table_columns_cache(table_name) - end - - def change_column_default(table_name, column_name, default) #:nodoc: - execute "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} DEFAULT #{quote(default)}" - ensure - clear_table_columns_cache(table_name) - end - - def change_column_null(table_name, column_name, null, default = nil) #:nodoc: - column = column_for(table_name, column_name) - - unless null || default.nil? - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") - end - - change_column table_name, column_name, column.sql_type, :null => null - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - column = column_for(table_name, column_name) - - # remove :null option if its value is the same as current column definition - # otherwise Oracle will raise error - if options.has_key?(:null) && options[:null] == column.null - options[:null] = nil - end - if type.to_sym == :virtual - type = options[:type] - end - change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} " - change_column_sql << "#{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" if type - - add_column_options!(change_column_sql, options.merge(:type=>type, :column_name=>column_name, :table_name=>table_name)) - - change_column_sql << tablespace_for((type_to_sql(type).downcase.to_sym), nil, options[:table_name], options[:column_name]) if type - - execute(change_column_sql) - ensure - clear_table_columns_cache(table_name) - end - - def rename_column(table_name, column_name, new_column_name) #:nodoc: - execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} to #{quote_column_name(new_column_name)}" - self.all_schema_indexes = nil - rename_column_indexes(table_name, column_name, new_column_name) - ensure - clear_table_columns_cache(table_name) - end - - def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc: - execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}" - ensure - clear_table_columns_cache(table_name) - self.all_schema_indexes = nil - end - - def add_comment(table_name, column_name, comment) #:nodoc: - return if comment.blank? - execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{column_name} IS '#{comment}'" - end - - def add_table_comment(table_name, comment) #:nodoc: - return if comment.blank? - execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS '#{comment}'" - end - - def table_comment(table_name) #:nodoc: - (owner, table_name, db_link) = @connection.describe(table_name) - select_value <<-SQL - SELECT comments FROM all_tab_comments#{db_link} - WHERE owner = '#{owner}' - AND table_name = '#{table_name}' - SQL - end - - def column_comment(table_name, column_name) #:nodoc: - (owner, table_name, db_link) = @connection.describe(table_name) - select_value <<-SQL - SELECT comments FROM all_col_comments#{db_link} - WHERE owner = '#{owner}' - AND table_name = '#{table_name}' - AND column_name = '#{column_name.upcase}' - SQL - end - - # Maps logical Rails types to Oracle-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: - # Ignore options for :text and :binary columns - return super(type, nil, nil, nil) if ['text', 'binary'].include?(type.to_s) - - super - end - - def tablespace(table_name) - select_value <<-SQL - SELECT tablespace_name - FROM user_tables - WHERE table_name='#{table_name.to_s.upcase}' - SQL - end - - private - - def tablespace_for(obj_type, tablespace_option, table_name=nil, column_name=nil) - tablespace_sql = '' - if tablespace = (tablespace_option || default_tablespace_for(obj_type)) - tablespace_sql << if [:blob, :clob].include?(obj_type.to_sym) - " LOB (#{quote_column_name(column_name)}) STORE AS #{column_name.to_s[0..10]}_#{table_name.to_s[0..14]}_ls (TABLESPACE #{tablespace})" - else - " TABLESPACE #{tablespace}" - end - end - tablespace_sql - end - - def default_tablespace_for(type) - (default_tablespaces[type] || default_tablespaces[native_database_types[type][:name]]) rescue nil - end - - - def column_for(table_name, column_name) - unless column = columns(table_name).find { |c| c.name == column_name.to_s } - raise "No such column: #{table_name}.#{column_name}" - end - column - end - - def create_sequence_and_trigger(table_name, options) - seq_name = options[:sequence_name] || default_sequence_name(table_name) - seq_start_value = options[:sequence_start_value] || default_sequence_start_value - execute "CREATE SEQUENCE #{quote_table_name(seq_name)} START WITH #{seq_start_value}" - - create_primary_key_trigger(table_name, options) if options[:primary_key_trigger] - end - - def create_primary_key_trigger(table_name, options) - seq_name = options[:sequence_name] || default_sequence_name(table_name) - trigger_name = options[:trigger_name] || default_trigger_name(table_name) - primary_key = options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize) - execute compress_lines(<<-SQL) - CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)} - BEFORE INSERT ON #{quote_table_name(table_name)} FOR EACH ROW - BEGIN - IF inserting THEN - IF :new.#{quote_column_name(primary_key)} IS NULL THEN - SELECT #{quote_table_name(seq_name)}.NEXTVAL INTO :new.#{quote_column_name(primary_key)} FROM dual; - END IF; - END IF; - END; - SQL - end - - def default_trigger_name(table_name) - # truncate table name if necessary to fit in max length of identifier - "#{table_name.to_s[0,table_name_length-4]}_pkt" - end - - end - end -end - -ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do - include ActiveRecord::ConnectionAdapters::OracleEnhancedSchemaStatements -end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb b/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb deleted file mode 100644 index 9629b49d2..000000000 --- a/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +++ /dev/null @@ -1,258 +0,0 @@ -require 'digest/sha1' - -module ActiveRecord - module ConnectionAdapters - module OracleEnhancedSchemaStatementsExt - def supports_foreign_keys? #:nodoc: - true - end - - # Create primary key trigger (so that you can skip primary key value in INSERT statement). - # By default trigger name will be "table_name_pkt", you can override the name with - # :trigger_name option (but it is not recommended to override it as then this trigger will - # not be detected by ActiveRecord model and it will still do prefetching of sequence value). - # - # add_primary_key_trigger :users - # - # You can also create primary key trigger using +create_table+ with :primary_key_trigger - # option: - # - # create_table :users, :primary_key_trigger => true do |t| - # # ... - # end - # - def add_primary_key_trigger(table_name, options={}) - # call the same private method that is used for create_table :primary_key_trigger => true - create_primary_key_trigger(table_name, options) - end - - # Adds a new foreign key to the +from_table+, referencing the primary key of +to_table+ - # (syntax and partial implementation taken from http://github.com/matthuhiggins/foreigner) - # - # The foreign key will be named after the from and to tables unless you pass - # :name as an option. - # - # === Examples - # ==== Creating a foreign key - # add_foreign_key(:comments, :posts) - # generates - # ALTER TABLE comments ADD CONSTRAINT - # comments_post_id_fk FOREIGN KEY (post_id) REFERENCES posts (id) - # - # ==== Creating a named foreign key - # add_foreign_key(:comments, :posts, :name => 'comments_belongs_to_posts') - # generates - # ALTER TABLE comments ADD CONSTRAINT - # comments_belongs_to_posts FOREIGN KEY (post_id) REFERENCES posts (id) - # - # ==== Creating a cascading foreign_key on a custom column - # add_foreign_key(:people, :people, :column => 'best_friend_id', :dependent => :nullify) - # generates - # ALTER TABLE people ADD CONSTRAINT - # people_best_friend_id_fk FOREIGN KEY (best_friend_id) REFERENCES people (id) - # ON DELETE SET NULL - # - # ==== Creating a composite foreign key - # add_foreign_key(:comments, :posts, :columns => ['post_id', 'author_id'], :name => 'comments_post_fk') - # generates - # ALTER TABLE comments ADD CONSTRAINT - # comments_post_fk FOREIGN KEY (post_id, author_id) REFERENCES posts (post_id, author_id) - # - # === Supported options - # [:column] - # Specify the column name on the from_table that references the to_table. By default this is guessed - # to be the singular name of the to_table with "_id" suffixed. So a to_table of :posts will use "post_id" - # as the default :column. - # [:columns] - # An array of column names when defining composite foreign keys. An alias of :column provided for improved readability. - # [:primary_key] - # Specify the column name on the to_table that is referenced by this foreign key. By default this is - # assumed to be "id". Ignored when defining composite foreign keys. - # [:name] - # Specify the name of the foreign key constraint. This defaults to use from_table and foreign key column. - # [:dependent] - # If set to :delete, the associated records in from_table are deleted when records in to_table table are deleted. - # If set to :nullify, the foreign key column is set to +NULL+. - def add_foreign_key(from_table, to_table, options = {}) - columns = options[:column] || options[:columns] || "#{to_table.to_s.singularize}_id" - constraint_name = foreign_key_constraint_name(from_table, columns, options) - sql = "ALTER TABLE #{quote_table_name(from_table)} ADD CONSTRAINT #{quote_column_name(constraint_name)} " - sql << foreign_key_definition(to_table, options) - execute sql - end - - def foreign_key_definition(to_table, options = {}) #:nodoc: - columns = Array(options[:column] || options[:columns]) - - if columns.size > 1 - # composite foreign key - columns_sql = columns.map {|c| quote_column_name(c)}.join(',') - references = options[:references] || columns - references_sql = references.map {|c| quote_column_name(c)}.join(',') - else - columns_sql = quote_column_name(columns.first || "#{to_table.to_s.singularize}_id") - references = options[:references] ? options[:references].first : nil - references_sql = quote_column_name(options[:primary_key] || references || "id") - end - - table_name = ActiveRecord::Migrator.proper_table_name(to_table) - - sql = "FOREIGN KEY (#{columns_sql}) REFERENCES #{quote_table_name(table_name)}(#{references_sql})" - - case options[:dependent] - when :nullify - sql << " ON DELETE SET NULL" - when :delete - sql << " ON DELETE CASCADE" - end - sql - end - - # Remove the given foreign key from the table. - # - # ===== Examples - # ====== Remove the suppliers_company_id_fk in the suppliers table. - # remove_foreign_key :suppliers, :companies - # ====== Remove the foreign key named accounts_branch_id_fk in the accounts table. - # remove_foreign_key :accounts, :column => :branch_id - # ====== Remove the foreign key named party_foreign_key in the accounts table. - # remove_foreign_key :accounts, :name => :party_foreign_key - def remove_foreign_key(from_table, options) - if Hash === options - constraint_name = foreign_key_constraint_name(from_table, options[:column], options) - else - constraint_name = foreign_key_constraint_name(from_table, "#{options.to_s.singularize}_id") - end - execute "ALTER TABLE #{quote_table_name(from_table)} DROP CONSTRAINT #{quote_column_name(constraint_name)}" - end - - private - - def foreign_key_constraint_name(table_name, columns, options = {}) - columns = Array(columns) - constraint_name = original_name = options[:name] || "#{table_name}_#{columns.join('_')}_fk" - - return constraint_name if constraint_name.length <= OracleEnhancedAdapter::IDENTIFIER_MAX_LENGTH - - # leave just first three letters from each word - constraint_name = constraint_name.split('_').map{|w| w[0,3]}.join('_') - # generate unique name using hash function - if constraint_name.length > OracleEnhancedAdapter::IDENTIFIER_MAX_LENGTH - constraint_name = 'c'+Digest::SHA1.hexdigest(original_name)[0,OracleEnhancedAdapter::IDENTIFIER_MAX_LENGTH-1] - end - @logger.warn "#{adapter_name} shortened foreign key constraint name #{original_name} to #{constraint_name}" if @logger - constraint_name - end - - - public - - # get table foreign keys for schema dump - def foreign_keys(table_name) #:nodoc: - (owner, desc_table_name, db_link) = @connection.describe(table_name) - - fk_info = select_all(<<-SQL, 'Foreign Keys') - SELECT r.table_name to_table - ,rc.column_name references_column - ,cc.column_name - ,c.constraint_name name - ,c.delete_rule - FROM user_constraints#{db_link} c, user_cons_columns#{db_link} cc, - user_constraints#{db_link} r, user_cons_columns#{db_link} rc - WHERE c.owner = '#{owner}' - AND c.table_name = '#{desc_table_name}' - AND c.constraint_type = 'R' - AND cc.owner = c.owner - AND cc.constraint_name = c.constraint_name - AND r.constraint_name = c.r_constraint_name - AND r.owner = c.owner - AND rc.owner = r.owner - AND rc.constraint_name = r.constraint_name - AND rc.position = cc.position - ORDER BY name, to_table, column_name, references_column - SQL - - fks = {} - - fk_info.map do |row| - name = oracle_downcase(row['name']) - fks[name] ||= { :columns => [], :to_table => oracle_downcase(row['to_table']), :references => [] } - fks[name][:columns] << oracle_downcase(row['column_name']) - fks[name][:references] << oracle_downcase(row['references_column']) - case row['delete_rule'] - when 'CASCADE' - fks[name][:dependent] = :delete - when 'SET NULL' - fks[name][:dependent] = :nullify - end - end - - fks.map do |k, v| - options = {:name => k, :columns => v[:columns], :references => v[:references], :dependent => v[:dependent]} - OracleEnhancedForeignKeyDefinition.new(table_name, v[:to_table], options) - end - end - - # REFERENTIAL INTEGRITY ==================================== - - def disable_referential_integrity(&block) #:nodoc: - sql_constraints = <<-SQL - SELECT constraint_name, owner, table_name - FROM user_constraints - WHERE constraint_type = 'R' - AND status = 'ENABLED' - SQL - old_constraints = select_all(sql_constraints) - begin - old_constraints.each do |constraint| - execute "ALTER TABLE #{constraint["table_name"]} DISABLE CONSTRAINT #{constraint["constraint_name"]}" - end - yield - ensure - old_constraints.each do |constraint| - execute "ALTER TABLE #{constraint["table_name"]} ENABLE CONSTRAINT #{constraint["constraint_name"]}" - end - end - end - - # Add synonym to existing table or view or sequence. Can be used to create local synonym to - # remote table in other schema or in other database - # Examples: - # - # add_synonym :posts, "blog.posts" - # add_synonym :posts_seq, "blog.posts_seq" - # add_synonym :employees, "hr.employees@dblink", :force => true - # - def add_synonym(name, table_name, options = {}) - sql = "CREATE" - if options[:force] == true - sql << " OR REPLACE" - end - sql << " SYNONYM #{quote_table_name(name)} FOR #{quote_table_name(table_name)}" - execute sql - end - - # Remove existing synonym to table or view or sequence - # Example: - # - # remove_synonym :posts, "blog.posts" - # - def remove_synonym(name) - execute "DROP SYNONYM #{quote_table_name(name)}" - end - - # get synonyms for schema dump - def synonyms #:nodoc: - select_all("SELECT synonym_name, table_owner, table_name, db_link FROM user_synonyms").collect do |row| - OracleEnhancedSynonymDefinition.new(oracle_downcase(row['synonym_name']), - oracle_downcase(row['table_owner']), oracle_downcase(row['table_name']), oracle_downcase(row['db_link'])) - end - end - - end - end -end - -ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do - include ActiveRecord::ConnectionAdapters::OracleEnhancedSchemaStatementsExt -end diff --git a/lib/active_record/connection_adapters/oracle_enhanced_version.rb b/lib/active_record/connection_adapters/oracle_enhanced_version.rb deleted file mode 100644 index d02d8b98c..000000000 --- a/lib/active_record/connection_adapters/oracle_enhanced_version.rb +++ /dev/null @@ -1 +0,0 @@ -ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter::VERSION = File.read(File.expand_path('../../../../VERSION', __FILE__)).chomp diff --git a/lib/active_record/oracle_enhanced/type/integer.rb b/lib/active_record/oracle_enhanced/type/integer.rb new file mode 100644 index 000000000..c4c82e722 --- /dev/null +++ b/lib/active_record/oracle_enhanced/type/integer.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module OracleEnhanced + module Type + class Integer < ActiveRecord::Type::Integer # :nodoc: + private + + def max_value + ("9"*38).to_i + end + end + end + end +end diff --git a/lib/active_record/oracle_enhanced/type/raw.rb b/lib/active_record/oracle_enhanced/type/raw.rb new file mode 100644 index 000000000..c27aba6e3 --- /dev/null +++ b/lib/active_record/oracle_enhanced/type/raw.rb @@ -0,0 +1,13 @@ +require 'active_record/type/string' + +module ActiveRecord + module OracleEnhanced + module Type + class Raw < ActiveRecord::Type::String # :nodoc: + def type + :raw + end + end + end + end +end diff --git a/lib/active_record/oracle_enhanced/type/timestamp.rb b/lib/active_record/oracle_enhanced/type/timestamp.rb new file mode 100644 index 000000000..020ceaf98 --- /dev/null +++ b/lib/active_record/oracle_enhanced/type/timestamp.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module OracleEnhanced + module Type + class Timestamp < ActiveRecord::Type::Value # :nodoc: + def type + :timestamp + end + end + end + end +end diff --git a/lib/activerecord-oracle_enhanced-adapter.rb b/lib/pmacs-activerecord-oracle_enhanced-adapter.rb similarity index 97% rename from lib/activerecord-oracle_enhanced-adapter.rb rename to lib/pmacs-activerecord-oracle_enhanced-adapter.rb index 00d2ee387..8a3ab0c3c 100644 --- a/lib/activerecord-oracle_enhanced-adapter.rb +++ b/lib/pmacs-activerecord-oracle_enhanced-adapter.rb @@ -5,7 +5,7 @@ module ActiveRecord module ConnectionAdapters class OracleEnhancedRailtie < ::Rails::Railtie rake_tasks do - load 'active_record/connection_adapters/oracle_enhanced_database_tasks.rb' + load 'active_record/connection_adapters/oracle_enhanced/database_tasks.rb' end ActiveSupport.on_load(:active_record) do diff --git a/activerecord-oracle_enhanced-adapter.gemspec b/pmacs-activerecord-oracle_enhanced-adapter.gemspec similarity index 69% rename from activerecord-oracle_enhanced-adapter.gemspec rename to pmacs-activerecord-oracle_enhanced-adapter.gemspec index 77dc07525..deae7ae8a 100644 --- a/activerecord-oracle_enhanced-adapter.gemspec +++ b/pmacs-activerecord-oracle_enhanced-adapter.gemspec @@ -4,17 +4,17 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |s| - s.name = %q{activerecord-oracle_enhanced-adapter} - s.version = "1.5.6" + s.name = %q{pmacs-activerecord-oracle_enhanced-adapter} + s.version = "1.6.2.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.license = 'MIT' - s.authors = [%q{Raimonds Simanovskis}] - s.date = %q{2015-03-30} + s.authors = [%q{Charles Treatman}, %q{Raimonds Simanovskis}] + s.date = %q{2015-08-12} s.description = %q{Oracle "enhanced" ActiveRecord adapter contains useful additional methods for working with new and legacy Oracle databases. This adapter is superset of original ActiveRecord Oracle adapter. } - s.email = %q{raimonds.simanovskis@gmail.com} + s.email = %q{charles.treatman@gmail.com} s.extra_rdoc_files = [ "README.md" ] @@ -27,27 +27,31 @@ This adapter is superset of original ActiveRecord Oracle adapter. "RUNNING_TESTS.md", "Rakefile", "VERSION", - "activerecord-oracle_enhanced-adapter.gemspec", + "pmacs-activerecord-oracle_enhanced-adapter.gemspec", "lib/active_record/connection_adapters/emulation/oracle_adapter.rb", "lib/active_record/connection_adapters/oracle_enhanced_adapter.rb", - "lib/active_record/connection_adapters/oracle_enhanced_column.rb", - "lib/active_record/connection_adapters/oracle_enhanced_column_dumper.rb", - "lib/active_record/connection_adapters/oracle_enhanced_connection.rb", - "lib/active_record/connection_adapters/oracle_enhanced_context_index.rb", - "lib/active_record/connection_adapters/oracle_enhanced_cpk.rb", - "lib/active_record/connection_adapters/oracle_enhanced_database_statements.rb", - "lib/active_record/connection_adapters/oracle_enhanced_dirty.rb", - "lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb", - "lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb", - "lib/active_record/connection_adapters/oracle_enhanced_procedures.rb", - "lib/active_record/connection_adapters/oracle_enhanced_schema_creation.rb", - "lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb", - "lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb", - "lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb", - "lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb", - "lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb", - "lib/active_record/connection_adapters/oracle_enhanced_version.rb", - "lib/activerecord-oracle_enhanced-adapter.rb", + "lib/active_record/connection_adapters/oracle_enhanced/column.rb", + "lib/active_record/connection_adapters/oracle_enhanced/column_dumper.rb", + "lib/active_record/connection_adapters/oracle_enhanced/connection.rb", + "lib/active_record/connection_adapters/oracle_enhanced/context_index.rb", + "lib/active_record/connection_adapters/oracle_enhanced/cpk.rb", + "lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb", + "lib/active_record/connection_adapters/oracle_enhanced/dirty.rb", + "lib/active_record/connection_adapters/oracle_enhanced/database_tasks.rb", + "lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb", + "lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb", + "lib/active_record/connection_adapters/oracle_enhanced/procedures.rb", + "lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb", + "lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb", + "lib/active_record/connection_adapters/oracle_enhanced/schema_dumper.rb", + "lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb", + "lib/active_record/connection_adapters/oracle_enhanced/schema_statements_ext.rb", + "lib/active_record/connection_adapters/oracle_enhanced/structure_dump.rb", + "lib/active_record/connection_adapters/oracle_enhanced/version.rb", + "lib/active_record/oracle_enhanced/type/integer.rb", + "lib/active_record/oracle_enhanced/type/timestamp.rb", + "lib/active_record/oracle_enhanced/type/raw.rb", + "lib/pmacs-activerecord-oracle_enhanced-adapter.rb", "spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb", "spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb", "spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb", @@ -63,9 +67,9 @@ This adapter is superset of original ActiveRecord Oracle adapter. "spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb", "spec/spec_helper.rb" ] - s.homepage = %q{http://github.com/rsim/oracle-enhanced} + s.homepage = %q{http://github.com/pmacs/oracle-enhanced} s.require_paths = [%q{lib}] - s.rubygems_version = %q{1.8.6} + s.rubygems_version = %q{2.2.2} s.summary = %q{Oracle enhanced adapter for ActiveRecord} s.test_files = [ "spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb", @@ -88,9 +92,10 @@ This adapter is superset of original ActiveRecord Oracle adapter. s.specification_version = 3 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_development_dependency(%q, ["~> 1.8"]) + s.add_development_dependency(%q, ["~> 2.0"]) s.add_development_dependency(%q, ["~> 2.4"]) - s.add_development_dependency(%q, [">= 0"]) + s.add_development_dependency(%q, [">= 0"]) + s.add_development_dependency(%q, ["~> 4.2.1"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) @@ -100,9 +105,10 @@ This adapter is superset of original ActiveRecord Oracle adapter. s.add_development_dependency(%q, [">= 0.4.4"]) s.add_development_dependency(%q, [">= 2.0.4"]) else - s.add_dependency(%q, ["~> 1.8"]) + s.add_dependency(%q, ["~> 2.0"]) s.add_dependency(%q, ["~> 2.4"]) - s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, ["~> 4.2.1"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) @@ -113,9 +119,10 @@ This adapter is superset of original ActiveRecord Oracle adapter. s.add_dependency(%q, [">= 2.0.4"]) end else - s.add_dependency(%q, ["~> 1.8"]) + s.add_dependency(%q, ["~> 2.0"]) s.add_dependency(%q, ["~> 2.4"]) - s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, ["~> 4.2.1"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) diff --git a/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb index 2bd502d95..9643c3786 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb @@ -476,7 +476,7 @@ class ::CamelCase < ActiveRecord::Base t.string :title # cannot update LOBs over database link t.string :body - t.timestamps + t.timestamps null: true end @db_link_username = SYSTEM_CONNECTION_PARAMS[:username] @db_link_password = SYSTEM_CONNECTION_PARAMS[:password] @@ -628,8 +628,8 @@ class ::TestPost < ActiveRecord::Base end it "should clear older cursors when statement limit is reached" do - pk = TestPost.columns.find { |c| c.primary } - sub = @conn.substitute_at(pk, 0) + pk = TestPost.columns_hash[TestPost.primary_key] + sub = @conn.substitute_at(pk, 0).to_sql binds = [[pk, 1]] lambda { @@ -641,8 +641,8 @@ class ::TestPost < ActiveRecord::Base it "should cache UPDATE statements with bind variables" do lambda { - pk = TestPost.columns.find { |c| c.primary } - sub = @conn.substitute_at(pk, 0) + pk = TestPost.columns_hash[TestPost.primary_key] + sub = @conn.substitute_at(pk, 0).to_sql binds = [[pk, 1]] @conn.exec_update("UPDATE test_posts SET id = #{sub}", "SQL", binds) }.should change(@statements, :length).by(+1) @@ -682,36 +682,11 @@ class ::TestPost < ActiveRecord::Base end it "should explain query with binds" do - pk = TestPost.columns.find { |c| c.primary } + pk = TestPost.columns_hash[TestPost.primary_key] sub = @conn.substitute_at(pk, 0) explain = TestPost.where(TestPost.arel_table[pk.name].eq(sub)).bind([pk, 1]).explain explain.should include("Cost") explain.should include("INDEX UNIQUE SCAN") end end if ENV['RAILS_GEM_VERSION'] >= '3.2' - - describe ".is_integer_column?" do - before(:all) do - @adapter = ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter - end - - it "should return TrueClass or FalseClass" do - @adapter.is_integer_column?("adapter_id").should be_a TrueClass - @adapter.is_integer_column?("").should be_a FalseClass - end - - it "should return true if name is 'id'" do - @adapter.is_integer_column?("id").should be_true - end - - it "should return true if name ends with '_id'" do - @adapter.is_integer_column?("_id").should be_true - @adapter.is_integer_column?("foo_id").should be_true - end - - it "should return false if name is 'something_else'" do - @adapter.is_integer_column?("something_else").should be_false - end - end - end diff --git a/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb index 7256810a6..0ceb27965 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb @@ -227,7 +227,7 @@ def lookup(path) it "should execute prepared statement with decimal bind parameter " do cursor = @conn.prepare("INSERT INTO test_employees VALUES(:1)") - column = ActiveRecord::ConnectionAdapters::OracleEnhancedColumn.new('age', nil, 'NUMBER(10,2)') + column = ActiveRecord::ConnectionAdapters::OracleEnhancedColumn.new('age', nil, ActiveRecord::Type::Decimal.new, 'NUMBER(10,2)') column.type.should == :decimal cursor.bind_param(1, "1.5", column) cursor.exec diff --git a/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb index 2b4356d81..174dc30fb 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb @@ -11,7 +11,7 @@ def create_table_posts t.string :title t.text :body t.integer :comments_count - t.timestamps + t.timestamps null: true t.string :all_text, limit: 2 # will be used for multi-column index end end @@ -23,7 +23,7 @@ def create_table_comments t.integer :post_id t.string :author t.text :body - t.timestamps + t.timestamps null: true end end end diff --git a/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb index 721e61aae..7dcbae1b1 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb @@ -67,12 +67,12 @@ class ::JobHistory < ActiveRecord::Base t.string :type_category, :limit => 15, :null => false t.date :date_value, :null => false t.text :results, :null => false - t.timestamps + t.timestamps null: true end create_table :non_cpk_write_lobs_test, :force => true do |t| t.date :date_value, :null => false t.text :results, :null => false - t.timestamps + t.timestamps null: true end end class ::CpkWriteLobsTest < ActiveRecord::Base diff --git a/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb index 24bbf11b1..4664b011d 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb @@ -67,21 +67,21 @@ ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates_by_column_name = false columns = @conn.columns('test_employees') column = columns.detect{|c| c.name == "hire_date"} - column.type_cast(Time.now).class.should == Time + column.type_cast_from_database(Time.now).class.should == Time end it "should return Date value from DATE column if column name contains 'date' and emulate_dates_by_column_name is true" do ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates_by_column_name = true columns = @conn.columns('test_employees') column = columns.detect{|c| c.name == "hire_date"} - column.type_cast(Time.now).class.should == Date + column.type_cast_from_database(Time.now).class.should == Date end it "should typecast DateTime value to Date value from DATE column if column name contains 'date' and emulate_dates_by_column_name is true" do ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates_by_column_name = true columns = @conn.columns('test_employees') column = columns.detect{|c| c.name == "hire_date"} - column.type_cast(DateTime.new(1900,1,1)).class.should == Date + column.type_cast_from_database(DateTime.new(1900,1,1)).class.should == Date end describe "/ DATE values from ActiveRecord model" do @@ -206,7 +206,6 @@ class ::TestEmployee < ActiveRecord::Base job_id NUMBER, salary NUMBER, commission_pct NUMBER(2,2), - unwise_name_id NUMBER(2,2), manager_id NUMBER(6), is_manager NUMBER(1), department_id NUMBER(4,0), @@ -219,46 +218,17 @@ class ::TestEmployee < ActiveRecord::Base INCREMENT BY 1 START WITH 10040 CACHE 20 NOORDER NOCYCLE SQL end - + after(:all) do @conn.execute "DROP TABLE test2_employees" @conn.execute "DROP SEQUENCE test2_employees_seq" end - context "when number_datatype_coercion is :decimal" do - before { ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.stub(:number_datatype_coercion).and_return(:decimal) } - - it "should set NUMBER column type as decimal if emulate_integers_by_column_name is false" do - ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = false - columns = @conn.columns('test2_employees') - column = columns.detect{|c| c.name == "job_id"} - column.type.should == :decimal - end - - it "should set NUMBER column type as decimal if column name is not 'id' and does not ends with '_id' and emulate_integers_by_column_name is true" do - ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = true - columns = @conn.columns('test2_employees') - column = columns.detect{|c| c.name == "salary"} - column.type.should == :decimal - end - end - - context "when number_datatype_coercion is :float" do - before { ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.stub(:number_datatype_coercion).and_return(:float) } - - it "should set NUMBER column type as float if emulate_integers_by_column_name is false" do - ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = false - columns = @conn.columns('test2_employees') - column = columns.detect{|c| c.name == "job_id"} - column.type.should == :float - end - - it "should set NUMBER column type as float if column name is not 'id' and does not ends with '_id' and emulate_integers_by_column_name is true" do - ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = true - columns = @conn.columns('test2_employees') - column = columns.detect{|c| c.name == "salary"} - column.type.should == :float - end + it "should set NUMBER column type as decimal if emulate_integers_by_column_name is false" do + ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = false + columns = @conn.columns('test2_employees') + column = columns.detect{|c| c.name == "job_id"} + column.type.should == :decimal end it "should set NUMBER column type as integer if emulate_integers_by_column_name is true" do @@ -270,24 +240,10 @@ class ::TestEmployee < ActiveRecord::Base column.type.should == :integer end - it "should set NUMBER(p,0) column type as integer" do + it "should set NUMBER column type as decimal if column name does not contain 'id' and emulate_integers_by_column_name is true" do ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = true columns = @conn.columns('test2_employees') - column = columns.detect{|c| c.name == "department_id"} - column.type.should == :integer - end - - it "should set NUMBER(p,s) column type as integer if column name ends with '_id' and emulate_integers_by_column_name is true" do - ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = true - columns = @conn.columns('test2_employees') - column = columns.detect{|c| c.name == "unwise_name_id"} - column.type.should == :integer - end - - it "should set NUMBER(p,s) column type as decimal if column name ends with '_id' and emulate_integers_by_column_name is false" do - ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = false - columns = @conn.columns('test2_employees') - column = columns.detect{|c| c.name == "unwise_name_id"} + column = columns.detect{|c| c.name == "salary"} column.type.should == :decimal end @@ -295,14 +251,14 @@ class ::TestEmployee < ActiveRecord::Base ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = false columns = @conn.columns('test2_employees') column = columns.detect{|c| c.name == "job_id"} - column.type_cast(1.0).class.should == BigDecimal + column.type_cast_from_database(1.0).class.should == BigDecimal end - it "should return Fixnum value from NUMBER column if column name ends with '_id' and emulate_integers_by_column_name is true" do + it "should return Fixnum value from NUMBER column if column name contains 'id' and emulate_integers_by_column_name is true" do ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = true columns = @conn.columns('test2_employees') column = columns.detect{|c| c.name == "job_id"} - column.type_cast(1.0).class.should == Fixnum + column.type_cast_from_database(1.0).class.should == Fixnum end describe "/ NUMBER values from ActiveRecord model" do @@ -310,7 +266,7 @@ class ::TestEmployee < ActiveRecord::Base class ::Test2Employee < ActiveRecord::Base end end - + after(:each) do Object.send(:remove_const, "Test2Employee") @conn.clear_types_for_columns @@ -452,7 +408,7 @@ def create_employee2 columns = @conn.columns('test3_employees') %w(has_email has_phone active_flag manager_yn).each do |col| column = columns.detect{|c| c.name == col} - column.type_cast("Y").class.should == String + column.type_cast_from_database("Y").class.should == String end end @@ -461,8 +417,8 @@ def create_employee2 columns = @conn.columns('test3_employees') %w(has_email has_phone active_flag manager_yn).each do |col| column = columns.detect{|c| c.name == col} - column.type_cast("Y").class.should == TrueClass - column.type_cast("N").class.should == FalseClass + column.type_cast_from_database("Y").class.should == TrueClass + column.type_cast_from_database("N").class.should == FalseClass end end @@ -650,6 +606,7 @@ class ::TestEmployee < ActiveRecord::Base end + describe "OracleEnhancedAdapter date and timestamp with different NLS date formats" do before(:all) do ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) @@ -906,7 +863,7 @@ class ::TestEmployee < ActiveRecord::Base @employee.reload @employee.last_login_at.should == @today.to_time end - + end describe "OracleEnhancedAdapter handling of CLOB columns" do @@ -1130,6 +1087,18 @@ class ::TestSerializeEmployee < ActiveRecord::Base @employee.reload @employee.comments.should == "initial serialized data" end + + it "should keep serialized data after save" do + @employee = Test2Employee.new + @employee.comments = {:length=>{:is=>1}} + @employee.save + @employee.reload + @employee.comments.should == {:length=>{:is=>1}} + @employee.comments = {:length=>{:is=>2}} + @employee.save + @employee.reload + @employee.comments.should == {:length=>{:is=>2}} + end end describe "OracleEnhancedAdapter handling of BLOB columns" do @@ -1383,6 +1352,7 @@ class ::TestEmployee < ActiveRecord::Base end end + describe "OracleEnhancedAdapter quoting of NCHAR and NVARCHAR2 columns" do before(:all) do ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) @@ -1443,3 +1413,45 @@ class ::TestItem < ActiveRecord::Base end end + +describe "OracleEnhancedAdapter handling of BINARY_FLOAT columns" do + before(:all) do + ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) + @conn = ActiveRecord::Base.connection + @conn.execute "DROP TABLE test2_employees" rescue nil + @conn.execute <<-SQL + CREATE TABLE test2_employees ( + id NUMBER PRIMARY KEY, + first_name VARCHAR2(20), + last_name VARCHAR2(25), + email VARCHAR2(25), + phone_number VARCHAR2(20), + hire_date DATE, + job_id NUMBER, + salary NUMBER, + commission_pct NUMBER(2,2), + hourly_rate BINARY_FLOAT, + manager_id NUMBER(6), + is_manager NUMBER(1), + department_id NUMBER(4,0), + created_at DATE + ) + SQL + @conn.execute "DROP SEQUENCE test2_employees_seq" rescue nil + @conn.execute <<-SQL + CREATE SEQUENCE test2_employees_seq MINVALUE 1 + INCREMENT BY 1 START WITH 10040 CACHE 20 NOORDER NOCYCLE + SQL + end + + after(:all) do + @conn.execute "DROP TABLE test2_employees" + @conn.execute "DROP SEQUENCE test2_employees_seq" + end + + it "should set BINARY_FLOAT column type as float" do + columns = @conn.columns('test2_employees') + column = columns.detect{|c| c.name == "hourly_rate"} + column.type.should == :float + end +end diff --git a/spec/active_record/connection_adapters/oracle_enhanced_database_tasks_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced_database_tasks_spec.rb index 54e196e3c..6f98925f1 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced_database_tasks_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced_database_tasks_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' -require 'active_record/connection_adapters/oracle_enhanced_database_tasks' +require 'active_record/connection_adapters/oracle_enhanced/database_tasks' require 'stringio' require 'tempfile' diff --git a/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb index 6dfbca9db..edb152af2 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb @@ -16,7 +16,6 @@ last_name VARCHAR2(25), job_id NUMBER(6,0) NULL, salary NUMBER(8,2), - pto_per_hour NUMBER, comments CLOB, hire_date DATE ) @@ -28,7 +27,7 @@ class TestEmployee < ActiveRecord::Base end end - + after(:all) do Object.send(:remove_const, "TestEmployee") @conn.execute "DROP TABLE test_employees" @@ -63,16 +62,6 @@ class TestEmployee < ActiveRecord::Base @employee.should_not be_changed end - it "should not mark empty float (stored as NULL) as changed when reassigning it" do - ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.stub(:number_datatype_coercion) { :float } - @employee = TestEmployee.create!(:pto_per_hour => '') - @employee.pto_per_hour = '' - @employee.should_not be_changed - @employee.reload - @employee.pto_per_hour = '' - @employee.should_not be_changed - end - it "should not mark empty text (stored as NULL) as changed when reassigning it" do @employee = TestEmployee.create!(:comments => nil) @employee.comments = nil @@ -122,7 +111,7 @@ class TestEmployee < ActiveRecord::Base @employee = TestEmployee.new @employee.job_id = 0 @employee.save!.should be_true - + @employee.should_not be_changed @employee.job_id = '0' @@ -147,6 +136,11 @@ class << oci_conn end end + it "should be able to handle attributes which are not backed by a column" do + TestEmployee.create!(:comments => "initial") + @employee = TestEmployee.select("#{TestEmployee.quoted_table_name}.*, 24 ranking").first + expect { @employee.ranking = 25 }.to_not raise_error + end end end diff --git a/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb index 490363faa..e7f49fef0 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb @@ -325,7 +325,8 @@ def raise_make_transaction_rollback :last_name => "Last", :hire_date => @today ) - @logger.logged(:debug).last.should match(/^TestEmployee Create \(\d+\.\d+(ms)?\) custom create method$/) + #TODO: dirty workaround to remove sql statement for `table` method + @logger.logged(:debug)[-2].should match(/^TestEmployee Create \(\d+\.\d+(ms)?\) custom create method$/) clear_logger end diff --git a/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb index c7a2daf65..4f7c7d1e0 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb @@ -22,7 +22,7 @@ def create_test_posts_table(options = {}) schema_define do create_table :test_posts, options do |t| t.string :title - t.timestamps + t.timestamps null: true end add_index :test_posts, :title end @@ -180,21 +180,21 @@ def drop_test_posts_table schema_define do add_foreign_key :test_comments, :test_posts end - standard_dump.should =~ /add_foreign_key "test_comments", "test_posts", name: "test_comments_test_post_id_fk"/ + standard_dump.should =~ /add_foreign_key "test_comments", "test_posts"/ end it "should include foreign key with delete dependency in schema dump" do schema_define do add_foreign_key :test_comments, :test_posts, dependent: :delete end - standard_dump.should =~ /add_foreign_key "test_comments", "test_posts", name: "test_comments_test_post_id_fk", dependent: :delete/ + standard_dump.should =~ /add_foreign_key "test_comments", "test_posts", on_delete: :cascade/ end it "should include foreign key with nullify dependency in schema dump" do schema_define do add_foreign_key :test_comments, :test_posts, dependent: :nullify end - standard_dump.should =~ /add_foreign_key "test_comments", "test_posts", name: "test_comments_test_post_id_fk", dependent: :nullify/ + standard_dump.should =~ /add_foreign_key "test_comments", "test_posts", on_delete: :nullify/ end it "should not include foreign keys on ignored table names in schema dump" do @@ -226,6 +226,7 @@ def drop_test_posts_table end it "should include composite foreign keys" do + pending "Composite foreign keys are not supported in this version" schema_define do add_column :test_posts, :baz_id, :integer add_column :test_posts, :fooz_id, :integer @@ -376,7 +377,7 @@ def drop_test_posts_table t.virtual :full_name, :as => "first_name || ', ' || last_name" t.virtual :short_name, :as => "COALESCE(first_name, last_name)", :type => :string, :limit => 300 t.virtual :abbrev_name, :as => "SUBSTR(first_name,1,50) || ' ' || SUBSTR(last_name,1,1) || '.'", :type => "VARCHAR(100)" - t.virtual :name_ratio, :as=>'(LENGTH(first_name)/LENGTH(last_name))' + t.virtual :name_ratio, :as=>'(LENGTH(first_name)*10/LENGTH(last_name)*10)' t.column :full_name_length, :virtual, :as => "length(first_name || ', ' || last_name)", :type => :integer t.virtual :field_with_leading_space, :as => "' ' || first_name || ' '", :limit => 300, :type => :string end @@ -402,30 +403,13 @@ class ::TestName < ActiveRecord::Base end end - context "when number_datatype_coercion is :float" do - before { ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.stub(:number_datatype_coercion).and_return(:float) } - - it 'should dump correctly' do - standard_dump.should =~ /t\.virtual "full_name",(\s*)limit: 512,(\s*)as: "\\"FIRST_NAME\\"\|\|', '\|\|\\"LAST_NAME\\"",(\s*)type: :string/ - standard_dump.should =~ /t\.virtual "short_name",(\s*)limit: 300,(\s*)as:(.*),(\s*)type: :string/ - standard_dump.should =~ /t\.virtual "full_name_length",(\s*)precision: 38,(\s*)scale: 0,(\s*)as:(.*),(\s*)type: :integer/ - standard_dump.should =~ /t\.virtual "name_ratio",(\s*)as:(.*),(\s*)type: :float$/ - standard_dump.should =~ /t\.virtual "abbrev_name",(\s*)limit: 100,(\s*)as:(.*),(\s*)type: :string/ - standard_dump.should =~ /t\.virtual "field_with_leading_space",(\s*)limit: 300,(\s*)as: "' '\|\|\\"FIRST_NAME\\"\|\|' '",(\s*)type: :string/ - end - end - - context "when number_datatype_coercion is :decimal" do - before { ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.stub(:number_datatype_coercion).and_return(:decimal) } - - it 'should dump correctly' do - standard_dump.should =~ /t\.virtual "full_name",(\s*)limit: 512,(\s*)as: "\\"FIRST_NAME\\"\|\|', '\|\|\\"LAST_NAME\\"",(\s*)type: :string/ - standard_dump.should =~ /t\.virtual "short_name",(\s*)limit: 300,(\s*)as:(.*),(\s*)type: :string/ - standard_dump.should =~ /t\.virtual "full_name_length",(\s*)precision: 38,(\s*)scale: 0,(\s*)as:(.*),(\s*)type: :integer/ - standard_dump.should =~ /t\.virtual "name_ratio",(\s*)as:(.*)\"$/ - standard_dump.should =~ /t\.virtual "abbrev_name",(\s*)limit: 100,(\s*)as:(.*),(\s*)type: :string/ - standard_dump.should =~ /t\.virtual "field_with_leading_space",(\s*)limit: 300,(\s*)as: "' '\|\|\\"FIRST_NAME\\"\|\|' '",(\s*)type: :string/ - end + it 'should dump correctly' do + standard_dump.should =~ /t\.virtual "full_name",(\s*)limit: 512,(\s*)as: "\\"FIRST_NAME\\"\|\|', '\|\|\\"LAST_NAME\\"",(\s*)type: :string/ + standard_dump.should =~ /t\.virtual "short_name",(\s*)limit: 300,(\s*)as:(.*),(\s*)type: :string/ + standard_dump.should =~ /t\.virtual "full_name_length",(\s*)precision: 38,(\s*)as:(.*),(\s*)type: :integer/ + standard_dump.should =~ /t\.virtual "name_ratio",(\s*)as:(.*)\"$/ # no :type + standard_dump.should =~ /t\.virtual "abbrev_name",(\s*)limit: 100,(\s*)as:(.*),(\s*)type: :string/ + standard_dump.should =~ /t\.virtual "field_with_leading_space",(\s*)limit: 300,(\s*)as: "' '\|\|\\"FIRST_NAME\\"\|\|' '",(\s*)type: :string/ end context 'with column cache' do @@ -471,160 +455,23 @@ class ::TestName < ActiveRecord::Base end end - describe "NUMBER columns" do - after(:each) do + describe ":float datatype" do + before(:each) do schema_define do - drop_table "test_numbers" - end - end - - let(:value_within_max_precision) { (10 ** @conn.class::NUMBER_MAX_PRECISION) - 1 } - let(:value_exceeding_max_precision) { (10 ** @conn.class::NUMBER_MAX_PRECISION) + 1 } - - context "when using ActiveRecord::Schema.define and ActiveRecord::ConnectionAdapters::TableDefinition#float" do - before :each do - schema_define do - create_table :test_numbers, :force => true do |t| - t.float :value - end - end - end - - context "when number_datatype_coercion is :float" do - before { ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.stub(:number_datatype_coercion).and_return(:float) } - - it "should dump correctly" do - standard_dump.should =~ /t\.float "value"$/ - end - end - - context "when number_datatype_coercion is :decimal" do - before { ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.stub(:number_datatype_coercion).and_return(:decimal) } - - it "should dump correctly" do - standard_dump.should =~ /t\.decimal "value"$/ + create_table :test_floats, force: true do |t| + t.float :hourly_rate end end end - context "when using handwritten 'CREATE_TABLE' SQL" do - before :each do - ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) - @conn = ActiveRecord::Base.connection - @conn.execute <<-SQL - CREATE TABLE test_numbers ( - id NUMBER(#{@conn.class::NUMBER_MAX_PRECISION},0) PRIMARY KEY, - value NUMBER - ) - SQL - @conn.execute <<-SQL - CREATE SEQUENCE test_numbers_seq MINVALUE 1 - INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER NOCYCLE - SQL - end - - context "when number_datatype_coercion is :float" do - before { ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.stub(:number_datatype_coercion).and_return(:float) } - - it "should dump correctly" do - standard_dump.should =~ /t\.float "value"$/ - end - - describe "ActiveRecord saving" do - before :each do - class ::TestNumber < ActiveRecord::Base - self.table_name = "test_numbers" - end - end - - it "should allow saving of values within NUMBER_MAX_PRECISION" do - number = TestNumber.new(value: value_within_max_precision) - number.save! - number.reload - number.value.should eq(value_within_max_precision) - end - - it "should allow saving of values larger than NUMBER_MAX_PRECISION" do - number = TestNumber.new(value: value_exceeding_max_precision) - number.save! - number.reload - number.value.should eq(value_exceeding_max_precision) - end - - it "should be recreatable from dump and have same properties" do - # Simulating db:schema:dump & db:test:load - 2.times do - create_table_dump = standard_dump[/(create_table.+?end)/m] - - schema_define do - drop_table "test_numbers" - end - - schema_define(&eval("-> * { #{create_table_dump} }")) - end - - number = TestNumber.new(value: value_within_max_precision) - number.save! - - number2 = TestNumber.new(value: value_exceeding_max_precision) - number2.save! - end - end + after(:each) do + schema_define do + drop_table :test_floats end + end - context "when number_datatype_coercion is :decimal" do - before { ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.stub(:number_datatype_coercion).and_return(:decimal) } - - it "should dump correctly" do - standard_dump.should =~ /t\.decimal "value"$/ - end - - describe "ActiveRecord saving" do - before :each do - class ::TestNumber < ActiveRecord::Base - self.table_name = "test_numbers" - end - end - - it "should allow saving of values within NUMBER_MAX_PRECISION" do - number = TestNumber.new(value: value_within_max_precision) - number.save! - number.reload - number.value.should eq(value_within_max_precision) - end - - it "should allow saving of values larger than NUMBER_MAX_PRECISION" do - number = TestNumber.new(value: value_exceeding_max_precision) - number.save! - number.reload - number.value.should eq(value_exceeding_max_precision) - end - - it "should be recreatable from dump and have same properties" do - # Simulating db:schema:dump & db:test:load - 2.times do |i| - create_table_dump = standard_dump[/(create_table.+?end)/m] - - schema_define do - drop_table "test_numbers" - end - - schema_define(&eval("-> * { #{create_table_dump} }")) - end - - number = TestNumber.new(value: value_within_max_precision) - number.save! - - # Raises 'ORA-01438' as :value column type isn't FLOAT'ish - number2 = TestNumber.new(value: value_exceeding_max_precision) - lambda do - number2.save! - end.should raise_error() { |e| e.message.should =~ /ORA-01438/ } - end - end - end # context (:decimal) - - end # context (handwritten) - end # describe (NUMBER columns) - + it "should dump float type correctly" do + standard_dump.should =~ /t\.float "hourly_rate"$/ + end + end end diff --git a/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb index 8090c474d..a4ee5d94c 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb @@ -627,25 +627,28 @@ class ::TestComment < ActiveRecord::Base end it "should add foreign key" do + fk_name = "fk_rails_#{Digest::SHA256.hexdigest("test_comments_test_post_id_fk").first(10)}" + schema_define do add_foreign_key :test_comments, :test_posts end lambda do TestComment.create(:body => "test", :test_post_id => 1) - end.should raise_error() {|e| e.message.should =~ /ORA-02291.*\.TEST_COMMENTS_TEST_POST_ID_FK/} + end.should raise_error() {|e| e.message.should =~ /ORA-02291.*\.#{fk_name}/i} end context "with table_name_prefix" do let(:table_name_prefix) { 'xxx_' } it "should use table_name_prefix for foreign table" do + fk_name = "fk_rails_#{Digest::SHA256.hexdigest("xxx_test_comments_test_post_id_fk").first(10)}" schema_define do add_foreign_key :test_comments, :test_posts end lambda do TestComment.create(:body => "test", :test_post_id => 1) - end.should raise_error() {|e| e.message.should =~ /ORA-02291.*\.XXX_TES_COM_TES_POS_ID_FK/} + end.should raise_error() {|e| e.message.should =~ /ORA-02291.*\.#{fk_name}/i} end end @@ -653,13 +656,14 @@ class ::TestComment < ActiveRecord::Base let(:table_name_suffix) { '_xxx' } it "should use table_name_suffix for foreign table" do + fk_name = "fk_rails_#{Digest::SHA256.hexdigest("test_comments_xxx_test_post_id_fk").first(10)}" schema_define do add_foreign_key :test_comments, :test_posts end lambda do TestComment.create(:body => "test", :test_post_id => 1) - end.should raise_error() {|e| e.message.should =~ /ORA-02291.*\.TES_COM_XXX_TES_POS_ID_FK/} + end.should raise_error() {|e| e.message.should =~ /ORA-02291.*\.#{fk_name}/i} end end @@ -678,7 +682,8 @@ class ::TestComment < ActiveRecord::Base end lambda do TestComment.create(:body => "test", :test_post_id => 1) - end.should raise_error() {|e| e.message.should =~ /ORA-02291.*\.TES_COM_TES_POS_ID_FOR_KEY/} + end.should raise_error() {|e| e.message.should =~ + /ORA-02291.*\.C#{Digest::SHA1.hexdigest("test_comments_test_post_id_foreign_key")[0,29].upcase}/} end it "should add foreign key with very long name which is shortened" do @@ -692,12 +697,14 @@ class ::TestComment < ActiveRecord::Base end it "should add foreign key with column" do + fk_name = "fk_rails_#{Digest::SHA256.hexdigest("test_comments_post_id_fk").first(10)}" + schema_define do add_foreign_key :test_comments, :test_posts, :column => "post_id" end lambda do TestComment.create(:body => "test", :post_id => 1) - end.should raise_error() {|e| e.message.should =~ /ORA-02291.*\.TEST_COMMENTS_POST_ID_FK/} + end.should raise_error() {|e| e.message.should =~ /ORA-02291.*\.#{fk_name}/i} end it "should add foreign key with delete dependency" do @@ -721,6 +728,7 @@ class ::TestComment < ActiveRecord::Base end it "should add a composite foreign key" do + pending "Composite foreign keys are not supported in this version" schema_define do add_column :test_posts, :baz_id, :integer add_column :test_posts, :fooz_id, :integer @@ -743,6 +751,7 @@ class ::TestComment < ActiveRecord::Base end it "should add a composite foreign key with name" do + pending "Composite foreign keys are not supported in this version" schema_define do add_column :test_posts, :baz_id, :integer add_column :test_posts, :fooz_id, :integer @@ -795,6 +804,45 @@ class ::TestComment < ActiveRecord::Base end + describe "lob in table definition" do + before do + class ::TestPost < ActiveRecord::Base + end + end + it 'should use default tablespace for clobs' do + ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces[:clob] = DATABASE_NON_DEFAULT_TABLESPACE + ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces[:blob] = nil + schema_define do + create_table :test_posts, :force => true do |t| + t.text :test_clob + t.binary :test_blob + end + end + TestPost.connection.select_value("SELECT tablespace_name FROM user_lobs WHERE table_name='TEST_POSTS' and column_name = 'TEST_CLOB'").should == DATABASE_NON_DEFAULT_TABLESPACE + TestPost.connection.select_value("SELECT tablespace_name FROM user_lobs WHERE table_name='TEST_POSTS' and column_name = 'TEST_BLOB'").should_not == DATABASE_NON_DEFAULT_TABLESPACE + end + + it 'should use default tablespace for blobs' do + ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces[:blob] = DATABASE_NON_DEFAULT_TABLESPACE + ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces[:clob] = nil + schema_define do + create_table :test_posts, :force => true do |t| + t.text :test_clob + t.binary :test_blob + end + end + TestPost.connection.select_value("SELECT tablespace_name FROM user_lobs WHERE table_name='TEST_POSTS' and column_name = 'TEST_BLOB'").should == DATABASE_NON_DEFAULT_TABLESPACE + TestPost.connection.select_value("SELECT tablespace_name FROM user_lobs WHERE table_name='TEST_POSTS' and column_name = 'TEST_CLOB'").should_not == DATABASE_NON_DEFAULT_TABLESPACE + end + + after do + Object.send(:remove_const, "TestPost") + schema_define do + drop_table :test_posts rescue nil + end + end + end + describe "foreign key in table definition" do before(:each) do schema_define do @@ -1359,6 +1407,13 @@ class <<@conn @would_execute_sql.should =~ /CREATE +INDEX .* ON .* \(.*\) TABLESPACE #{DATABASE_NON_DEFAULT_TABLESPACE}/ end + it "should create unique function index but not create unique constraints" do + schema_define do + add_index :keyboards, 'lower(name)', unique: true, name: :index_keyboards_on_lower_name + end + @would_execute_sql.should_not =~ /ALTER +TABLE .* ADD CONSTRAINT .* UNIQUE \(.*\(.*\)\)/ + end + describe "#initialize_schema_migrations_table" do # In Rails 2.3 to 3.2.x the index name for the migrations # table is hard-coded. We can modify the index name here diff --git a/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb index beb1dc128..be44bdf2f 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb @@ -92,6 +92,7 @@ class ::TestPost < ActiveRecord::Base end it "should dump composite foreign keys" do + pending "Composite foreign keys are not supported in this version" @conn.add_column :foos, :fooz_id, :integer @conn.add_column :foos, :baz_id, :integer diff --git a/spec/spec_config.yaml.template b/spec/spec_config.yaml.template new file mode 100644 index 000000000..f027ae316 --- /dev/null +++ b/spec/spec_config.yaml.template @@ -0,0 +1,10 @@ +rails: + gem_version: '4.0-master' +database: + name: 'orcl' + host: '127.0.0.1' + port: 1521 + user: 'oracle_enhanced' + password: 'oracle_enhanced' + sys_password: 'admin' +timezone: 'Europe/Riga' \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5fcaf3252..2c33952b7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,8 +1,17 @@ -require 'rubygems' +require "rubygems" require "bundler" +require "yaml" Bundler.setup(:default, :development) $:.unshift(File.expand_path('../../lib', __FILE__)) +config_path = File.expand_path('../spec_config.yaml', __FILE__) +if File.exist?(config_path) + puts "==> Loading config from #{config_path}" + config = YAML.load_file(config_path) +else + puts "==> Loading config from ENV or use default" + config = {"rails" => {}, "database" => {}} +end require 'rspec' @@ -13,10 +22,10 @@ puts "==> Running specs with JRuby version #{JRUBY_VERSION}" end -ENV['RAILS_GEM_VERSION'] ||= '4.0-master' +ENV['RAILS_GEM_VERSION'] ||= config["rails"]["gem_version"] || '4.0-master' NO_COMPOSITE_PRIMARY_KEYS = true -puts "==> Running specs with Rails version #{ENV['RAILS_GEM_VERSION']}" +puts "==> Selected Rails version #{ENV['RAILS_GEM_VERSION']}" require 'active_record' @@ -32,6 +41,8 @@ require 'active_record/connection_adapters/oracle_enhanced_adapter' require 'ruby-plsql' +puts "==> Effective ActiveRecord version #{ActiveRecord::VERSION::STRING}" + module LoggerSpecHelper def set_logger @logger = MockLogger.new @@ -109,12 +120,12 @@ def schema_define(&block) end end -DATABASE_NAME = ENV['DATABASE_NAME'] || 'orcl' -DATABASE_HOST = ENV['DATABASE_HOST'] -DATABASE_PORT = ENV['DATABASE_PORT'] -DATABASE_USER = ENV['DATABASE_USER'] || 'oracle_enhanced' -DATABASE_PASSWORD = ENV['DATABASE_PASSWORD'] || 'oracle_enhanced' -DATABASE_SYS_PASSWORD = ENV['DATABASE_SYS_PASSWORD'] || 'admin' +DATABASE_NAME = config["database"]["name"] || ENV['DATABASE_NAME'] || 'orcl' +DATABASE_HOST = config["database"]["host"] || ENV['DATABASE_HOST'] || "127.0.0.1" +DATABASE_PORT = config["database"]["port"] || ENV['DATABASE_PORT'] || 1521 +DATABASE_USER = config["database"]["user"] || ENV['DATABASE_USER'] || 'oracle_enhanced' +DATABASE_PASSWORD = config["database"]["password"] || ENV['DATABASE_PASSWORD'] || 'oracle_enhanced' +DATABASE_SYS_PASSWORD = config["database"]["sys_password"] || ENV['DATABASE_SYS_PASSWORD'] || 'admin' CONNECTION_PARAMS = { :adapter => "oracle_enhanced", @@ -148,7 +159,7 @@ def schema_define(&block) # set default time zone in TZ environment variable # which will be used to set session time zone -ENV['TZ'] ||= 'Europe/Riga' +ENV['TZ'] ||= config["timezone"] || 'Europe/Riga' # ActiveRecord::Base.logger = Logger.new(STDOUT)