require 'dm-migrations/auto_migration' require 'dm-migrations/adapters/dm-do-adapter' module DataMapper module Migrations module OracleAdapter include SQL, DataObjectsAdapter # @api private def self.included(base) base.extend DataObjectsAdapter::ClassMethods base.extend ClassMethods end # @api semipublic def storage_exists?(storage_name) statement = DataMapper::Ext::String.compress_lines(<<-SQL) SELECT COUNT(*) FROM all_tables WHERE owner = ? AND table_name = ? SQL select(statement, schema_name, oracle_upcase(storage_name)).first > 0 end # @api semipublic def sequence_exists?(sequence_name) return false unless sequence_name statement = DataMapper::Ext::String.compress_lines(<<-SQL) SELECT COUNT(*) FROM all_sequences WHERE sequence_owner = ? AND sequence_name = ? SQL select(statement, schema_name, oracle_upcase(sequence_name)).first > 0 end # @api semipublic def field_exists?(storage_name, field_name) statement = DataMapper::Ext::String.compress_lines(<<-SQL) SELECT COUNT(*) FROM all_tab_columns WHERE owner = ? AND table_name = ? AND column_name = ? SQL select(statement, schema_name, oracle_upcase(storage_name), oracle_upcase(field_name)).first > 0 end # @api semipublic def storage_fields(storage_name) statement = DataMapper::Ext::String.compress_lines(<<-SQL) SELECT column_name FROM all_tab_columns WHERE owner = ? AND table_name = ? SQL select(statement, schema_name, oracle_upcase(storage_name)) end def drop_table_statement(model) table_name = quote_name(model.storage_name(name)) "DROP TABLE #{table_name} CASCADE CONSTRAINTS" end # @api semipublic def create_model_storage(model) name = self.name properties = model.properties_with_subclasses(name) table_name = model.storage_name(name) truncate_or_delete = self.class.auto_migrate_with table_is_truncated = truncate_or_delete && @truncated_tables && @truncated_tables[table_name] return false if storage_exists?(table_name) && !table_is_truncated return false if properties.empty? with_connection do |connection| # if table was truncated then check if all columns for properties are present # TODO: check all other column definition options if table_is_truncated && storage_has_all_fields?(table_name, properties) @truncated_tables[table_name] = nil else # forced drop of table if properties are different destroy_model_storage(model, true) if truncate_or_delete statements = [create_table_statement(connection, model, properties)] statements.concat(create_index_statements(model)) statements.concat(create_unique_index_statements(model)) statements.concat(create_sequence_statements(model)) statements.each do |statement| command = connection.create_command(statement) command.execute_non_query end end end true end # @api semipublic def destroy_model_storage(model, forced = false) table_name = model.storage_name(name) klass = self.class truncate_or_delete = klass.auto_migrate_with if storage_exists?(table_name) if truncate_or_delete && !forced case truncate_or_delete when :truncate execute(truncate_table_statement(model)) when :delete execute(delete_table_statement(model)) else raise ArgumentError, 'Unsupported auto_migrate_with option' end @truncated_tables ||= {} @truncated_tables[table_name] = true else execute(drop_table_statement(model)) @truncated_tables[table_name] = nil if @truncated_tables end end # added destroy of sequences reset_sequences = klass.auto_migrate_reset_sequences table_is_truncated = @truncated_tables && @truncated_tables[table_name] unless truncate_or_delete && !reset_sequences && !forced if sequence_exists?(model_sequence_name(model)) statement = if table_is_truncated && !forced reset_sequence_statement(model) else drop_sequence_statement(model) end execute(statement) if statement end end true end private def storage_has_all_fields?(table_name, properties) properties.map { |property| oracle_upcase(property.field) }.sort == storage_fields(table_name).sort end # If table or column name contains just lowercase characters then do uppercase # as uppercase version will be used in Oracle data dictionary tables private def oracle_upcase(name) (name =~ /[A-Z]/) ? name : name.upcase end module SQL # :nodoc: # private ## This cannot be private for current migrations # @api private def schema_name @schema_name ||= select("SELECT SYS_CONTEXT('userenv','current_schema') FROM dual").first.freeze end # @api private def property_schema_hash(property) schema = super if property.is_a?(Property::Binary) # BLOB does not support length schema.delete(:length) end if property.primitive == String && property.length > 4000 # Oracle doesn't support string over 4000 bytes schema[:primitive] = 'NCLOB' # CLOB and NCLOB do not support length schema.delete(:length) end schema end # @api private def create_sequence_statements(model) name = self.name table_name = model.storage_name(name) serial = model.serial(name) statements = [] if (sequence_name = model_sequence_name(model)) sequence_name = quote_name(sequence_name) column_name = quote_name(serial.field) statements << DataMapper::Ext::String.compress_lines(<<-SQL) CREATE SEQUENCE #{sequence_name} NOCACHE SQL # create trigger only if custom sequence name was not specified unless serial.options[:sequence] statements << DataMapper::Ext::String.compress_lines(<<-SQL) CREATE OR REPLACE TRIGGER #{quote_name(default_trigger_name(table_name))} BEFORE INSERT ON #{quote_name(table_name)} FOR EACH ROW BEGIN IF inserting THEN IF :new.#{column_name} IS NULL THEN SELECT #{sequence_name}.NEXTVAL INTO :new.#{column_name} FROM dual; END IF; END IF; END; SQL end end statements end # @api private def drop_sequence_statement(model) return unless (sequence_name = model_sequence_name(model)) "DROP SEQUENCE #{quote_name(sequence_name)}" end # @api private def reset_sequence_statement(model) return unless (sequence_name = model_sequence_name(model)) sequence_name = quote_name(sequence_name) DataMapper::Ext::String.compress_lines(<<-SQL) DECLARE cval INTEGER; BEGIN SELECT #{sequence_name}.NEXTVAL INTO cval FROM dual; EXECUTE IMMEDIATE 'ALTER SEQUENCE #{sequence_name} INCREMENT BY -' || cval || ' MINVALUE 0'; SELECT #{sequence_name}.NEXTVAL INTO cval FROM dual; EXECUTE IMMEDIATE 'ALTER SEQUENCE #{sequence_name} INCREMENT BY 1'; END; SQL end # @api private def truncate_table_statement(model) "TRUNCATE TABLE #{quote_name(model.storage_name(name))}" end # @api private def delete_table_statement(model) "DELETE FROM #{quote_name(model.storage_name(name))}" end private def model_sequence_name(model) name = self.name table_name = model.storage_name(name) serial = model.serial(name) return unless serial serial.options[:sequence] || default_sequence_name(table_name) end private def default_sequence_name(table_name) # truncate table name if necessary to fit in max length of identifier "#{table_name[0, self.class::IDENTIFIER_MAX_LENGTH - 4]}_seq" end private def default_trigger_name(table_name) # truncate table name if necessary to fit in max length of identifier "#{table_name[0, self.class::IDENTIFIER_MAX_LENGTH - 4]}_pkt" end # @api private private def add_column_statement 'ADD' end end module ClassMethods # Types for Oracle databases. # # @return [Hash] types for Oracle databases. # # @api private def type_map length = Property::String.length precision = Property::Numeric.precision _scale = Property::Decimal.scale super.merge( Integer => {primitive: 'NUMBER', precision: precision, scale: 0}, String => {primitive: 'VARCHAR2', length: length}, Class => {primitive: 'VARCHAR2', length: length}, BigDecimal => {primitive: 'NUMBER', precision: precision, scale: nil}, Float => {primitive: 'BINARY_FLOAT'}, DateTime => {primitive: 'DATE'}, Date => {primitive: 'DATE'}, Time => {primitive: 'DATE'}, TrueClass => {primitive: 'NUMBER', precision: 1, scale: 0}, Property::Text => {primitive: 'CLOB'} ).freeze end # Use table truncate or delete for auto_migrate! to speed up test execution # # @param [Symbol] :truncate, :delete or :drop_and_create (or nil) # do not specify parameter to return current value # # @return [Mixed] # [Symbol] current value of auto_migrate_with option # (nil returned for :drop_and_create) # # @api semipublic def auto_migrate_with(value = :not_specified) return @auto_migrate_with if value == :not_specified value = nil if value == :drop_and_create raise ArgumentError unless [nil, :truncate, :delete].include?(value) @auto_migrate_with = value end # Set if sequences will or will not be reset during auto_migrate! # # @param [Mixed] value # reset sequences? TrueClass, FalseClass # do not specify parameter to return current value # # @return [Mixed] # [Symbol] current value of auto_migrate_reset_sequences option # (default value is true) # # @api semipublic def auto_migrate_reset_sequences(value = :not_specified) return @auto_migrate_reset_sequences.nil? ? true : @auto_migrate_reset_sequences if value == :not_specified raise ArgumentError unless [true, false].include?(value) @auto_migrate_reset_sequences = value end end end end end