Created
April 27, 2009 01:27
-
-
Save dcparker/102271 to your computer and use it in GitHub Desktop.
msaccess_adapter.rb for an older version of ActiveRecord
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'active_record/connection_adapters/abstract_adapter' | |
require 'bigdecimal' | |
require 'bigdecimal/util' | |
# msaccess_adapter.rb -- ActiveRecord adapter for Microsoft Access Db | |
# | |
# "Adapted" from the sqlserver_adapter.rb for Microsoft SQL Sever: | |
# Author: Joey Gibson <[email protected]> | |
# | |
# Modifications by DeLynn Berry <[email protected]>, | |
# Mark Imbriaco <[email protected]>, | |
# Tom Ward <[email protected]>, | |
# Ryan Tomayko <[email protected]> | |
# Up to July 2006 | |
# | |
# Converted to MSAccess (various functions ported over to ADO) by | |
# Daniel Parker <[email protected]> | |
# -- as current maintainer -- please send bugfixes! | |
module ActiveRecord | |
class Base | |
def self.msaccess_connection(config) #:nodoc: | |
require_library_or_gem 'win32ole' unless self.class.const_defined?(:WIN32OLE) | |
config = config.symbolize_keys | |
username = config[:username] ? config[:username].to_s : 'sa' | |
password = config[:password] ? config[:password].to_s : '' | |
autocommit = config.key?(:autocommit) ? config[:autocommit] : true | |
connection_string = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=#{config[:database]}" | |
driver_url = "DBI:ADO:#{connection_string}" | |
conn = WIN32OLE.new('ADODB.Connection') | |
conn.Open(connection_string) | |
# conn["AutoCommit"] = autocommit # Works with DBI, but not ADO | |
ConnectionAdapters::MsAccessAdapter.new(conn, logger, [driver_url, username, password]) | |
end | |
end # class Base | |
module ConnectionAdapters | |
class MsAccessColumn < Column# :nodoc: | |
attr_reader :is_special | |
attr_accessor :ole_column, :ole_table | |
#Human-readable Types: | |
# Text | |
# Number | |
# AutoNumber | |
# Currency | |
# Date/Time | |
# Yes/No | |
# Memo | |
# OLE Object? | |
#Types: | |
# adCurrency => Currency | |
# adVarWChar => Text | |
# adSmallInt => Number | |
# adUnsignedTinyInt => Number | |
# adDate => Date/Time | |
# Format: YYYY/MM/DD HH:MM:SS | |
# Quote: #{value}# | |
# adLongVarBinary => Number | |
# adDouble => Number | |
# adBoolean => Yes/No | |
# adInteger => Number | |
#For each type: | |
#1) Query value of type and see what kind of value it returns. | |
#2) Try to insert a value and see if it errors - see if inserting needs to be a different format | |
#3) Create the translation/mapping method(s) for the connector | |
#Test the whole thing | |
def initialize(name, default, ole_column, ole_table, sql_type = nil, null = true) # TODO: check ok to remove scale_value = 0 | |
super(name, default, sql_type, null) | |
self.ole_column = ole_column | |
self.ole_table = ole_table | |
@is_special = sql_type =~ /text|ntext|image/i # Why does this make it 'special'? | |
# TODO: check ok to remove @scale = scale_value | |
# @scale = scale_value | |
# # SQL Server only supports limits on *char and float types | |
# @limit = nil unless @type == :float or @type == :string | |
end | |
def simplified_type(field_type) | |
case field_type | |
when /real/i : :float | |
when /int|bigint|smallint|tinyint/i : :integer | |
when /float|double|decimal|money|numeric|real|smallmoney/i : @scale == 0 ? :integer : :float | |
when /datetime|smalldatetime/i : :datetime | |
when /timestamp/i : :timestamp | |
when /time/i : :time | |
when /text|ntext/i : :text | |
when /binary|image|varbinary/i : :binary | |
when /char|nchar|nvarchar|string|varchar/i : :string | |
when /bit/i : :boolean | |
else super | |
end | |
end | |
def type_cast(value) | |
return nil if value.nil? | |
case type | |
when :string then value | |
when :integer then value == true || value == false ? value == true ? '1' : '0' : value.to_i | |
when :float then value.to_f | |
when :datetime then cast_to_date_or_time(value) | |
when :timestamp then cast_to_time(value) | |
when :time then cast_to_time(value) | |
when :boolean then value == true or (value =~ /^t(rue)?$/i) == 0 or value.to_s == '1' | |
else super | |
end | |
end | |
def cast_to_time(value) | |
return value if value.is_a?(Time) | |
time_array = ParseDate.parsedate(value) | |
Time.send(Base.default_timezone, *time_array) rescue nil | |
end | |
def cast_to_datetime(value) | |
if value.is_a?(Time) | |
if value.year != 0 and value.month != 0 and value.day != 0 | |
return value | |
else | |
return Time.mktime(2000, 1, 1, value.hour, value.min, value.sec) rescue nil | |
end | |
end | |
if value.is_a?(DateTime) | |
return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec) | |
end | |
return cast_to_time(value) if value.is_a?(Date) or value.is_a?(String) rescue nil | |
value | |
end | |
# TODO: Find less hack way to convert DateTime objects into Times | |
def self.string_to_time(value) | |
if value.is_a?(DateTime) | |
return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec) | |
else | |
super | |
end | |
end | |
# These methods will only allow the adapter to insert binary data with a length of 7K or less | |
# because of a SQL Server statement length policy. | |
# These methods also need to be fixed for msaccess - they often cause errors because for some | |
# reason the value from the db can be an array. | |
def self.string_to_binary(value) | |
value.gsub(/(\r|\n|\0|\x1a)/) do | |
case $1 | |
when "\r" then "%00" | |
when "\n" then "%01" | |
when "\0" then "%02" | |
when "\x1a" then "%03" | |
end | |
end | |
end | |
def self.binary_to_string(value) | |
value.gsub(/(%00|%01|%02|%03)/) do | |
case $1 | |
when "%00" then "\r" | |
when "%01" then "\n" | |
when "%02\0" then "\0" | |
when "%03" then "\x1a" | |
end | |
end | |
end | |
def properties | |
return @properties unless @properties.blank? | |
@properties = [] | |
self.ole_column.Properties.each do |prop| | |
@properties << prop | |
end | |
@properties | |
end | |
def properties_hash | |
return @properties_hash if @properties_hash | |
@properties_hash = {} | |
self.properties.each do |p| | |
@properties_hash[p.Name] = p | |
end | |
@properties_hash | |
end | |
def property(prop) | |
self.properties_hash[prop] | |
end | |
def property_value(prop) | |
unquote_property_value(self.property(prop).Value) | |
end | |
def method_missing(method_name, *args) | |
return self.property_value(method_name) if method_name =~ /^[A-Z]/ && self.property(method_name) | |
super | |
end | |
private | |
def unquote_property_value(value) | |
if value =~ /\\?["'](.+)\\?["']/ | |
value = $1 | |
end | |
value | |
end | |
end | |
# In ADO mode, this adapter will ONLY work on Windows systems, | |
# since it relies on Win32OLE, which, to my knowledge, is only | |
# available on Windows. | |
# | |
# This mode also relies on the ADO support in the DBI module. If you are using the | |
# one-click installer of Ruby, then you already have DBI installed, but | |
# the ADO module is *NOT* installed. You will need to get the latest | |
# source distribution of Ruby-DBI from http://ruby-dbi.sourceforge.net/ | |
# unzip it, and copy the file | |
# <tt>src/lib/dbd_ado/ADO.rb</tt> | |
# to | |
# <tt>X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb</tt> | |
# (you will more than likely need to create the ADO directory). | |
# Once you've installed that file, you are ready to go. | |
# | |
# In ODBC mode, the adapter requires the ODBC support in the DBI module which requires | |
# the Ruby ODBC module. Ruby ODBC 0.996 was used in development and testing, | |
# and it is available at http://www.ch-werner.de/rubyodbc/ | |
# | |
# Options: | |
# | |
# * <tt>:mode</tt> -- ADO or ODBC. Defaults to ADO. | |
# * <tt>:username</tt> -- Defaults to sa. | |
# * <tt>:password</tt> -- Defaults to empty string. | |
# * <tt>:windows_auth</tt> -- Defaults to "User ID=#{username};Password=#{password}" | |
# | |
# ADO specific options: | |
# | |
# * <tt>:host</tt> -- Defaults to localhost. | |
# * <tt>:database</tt> -- The name of the database. No default, must be provided. | |
# * <tt>:windows_auth</tt> -- Use windows authentication instead of username/password. | |
# | |
# ODBC specific options: | |
# | |
# * <tt>:dsn</tt> -- Defaults to nothing. | |
# | |
# ADO code tested on Windows 2000 and higher systems, | |
# running ruby 1.8.2 (2004-07-29) [i386-mswin32], and SQL Server 2000 SP3. | |
# | |
# ODBC code tested on a Fedora Core 4 system, running FreeTDS 0.63, | |
# unixODBC 2.2.11, Ruby ODBC 0.996, Ruby DBI 0.0.23 and Ruby 1.8.2. | |
# [Linux strongmad 2.6.11-1.1369_FC4 #1 Thu Jun 2 22:55:56 EDT 2005 i686 i686 i386 GNU/Linux] | |
class MsAccessAdapter < AbstractAdapter | |
def initialize(connection, logger, connection_options=nil) | |
super(connection, logger) | |
@connection_options = connection_options | |
end | |
# Update this for typical MsAccess types? | |
def native_database_types | |
{ | |
:primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY", #Probably needs changed | |
:string => { :name => "varchar", :limit => 255 }, | |
:text => { :name => "text" }, | |
:integer => { :name => "int" }, | |
:float => { :name => "float", :limit => 8 }, | |
:decimal => { :name => "decimal" }, | |
:datetime => { :name => "datetime" }, | |
:timestamp => { :name => "datetime" }, | |
:time => { :name => "datetime" }, | |
:date => { :name => "datetime" }, | |
:binary => { :name => "image"}, | |
:boolean => { :name => "bit"} | |
} | |
end | |
def adapter_name | |
'MsAccess' | |
end | |
def supports_migrations? #:nodoc: | |
false | |
end | |
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: | |
return super unless type.to_s == 'integer' | |
if limit.nil? || limit == 4 | |
'integer' | |
elsif limit < 4 | |
'smallint' | |
else | |
'bigint' | |
end | |
end | |
# CONNECTION MANAGEMENT ====================================# | |
# Returns true if the connection is active. | |
def active? | |
# fix for MsAccess! | |
# @connection.Execute("SELECT 1").finish | |
true | |
rescue DBI::DatabaseError, DBI::InterfaceError | |
false | |
end | |
# Reconnects to the database, returns false if no connection could be made. | |
# fix for MsAccess! | |
def reconnect! | |
disconnect! | |
@connection = DBI.connect(*@connection_options) | |
rescue DBI::DatabaseError => e | |
@logger.warn "#{adapter_name} reconnection failed: #{e.message}" if @logger | |
false | |
end | |
# Disconnects from the database | |
# fix for MsAccess! | |
def disconnect! | |
@connection.disconnect rescue nil | |
end | |
# CATALOG FOR SCHEMA INFORMATION ====================================# | |
def catalog | |
return @catalog if @catalog | |
@catalog = WIN32OLE.new('ADOX.Catalog') | |
@catalog.ActiveConnection = @connection | |
@catalog | |
end | |
def tables_from_catalog | |
tables = [] | |
tables << self.catalog.Tables.each do |table| | |
tables << table | |
end | |
tables | |
end | |
def table_names_from_catalog | |
table_names = [] | |
table_names << self.catalog.Tables.each do |table| | |
table_names << table.Name | |
end | |
table_names | |
end | |
def content_tables | |
table_names_from_catalog.reject! {|t| t.nil? || t =~ /MSys/} | |
end | |
alias :tables :content_tables | |
def columns_from_catalog(table_name) | |
return self.catalog.Tables.Item(self.table_names_from_catalog.index(table_name)).Columns if ole_table(table_name) | |
end | |
def get_column_property(ole_column, property_name) | |
value = nil | |
ole_column.Properties.each do |property| | |
if property.Name == property_name | |
value = property.Value | |
end | |
end | |
# ole_value_to_string(value) | |
value | |
end | |
# probably unnecessary - replace with something better | |
def default_value_to_string(value) | |
case | |
when value.nil? : 'NULL' | |
when value.kind_of?(String) : value | |
else value.to_s | |
end | |
end | |
def columns(table_name, name = nil) | |
return [] if table_name.blank? | |
table_name = table_name.to_s if table_name.is_a?(Symbol) | |
table_name = table_name.split('.')[-1] unless table_name.nil? | |
table_name = table_name.gsub(/[\[\]]/, '') | |
# GATHER THE COLUMNS FROM MsAccess IN HERE | |
# column_name, default_value, data_type, is_nullable | |
columns = [] | |
result = [] | |
self.columns_from_catalog(table_name).each do |ole_column| | |
# With the properties and collections of a Column object, you can: | |
# | |
# Identify the column with the Name property. | |
# Specify the data type of the column with the Type property. | |
# Determine if the column is fixed-length, or if it can contain null values with the Attributes property. | |
# Specify the maximum size of the column with the DefinedSize property. | |
# For numeric data values, specify the scale with the NumericScale property. | |
# For numeric data value, specify the maximum precision with the Precision property. | |
# Specify the Catalog that owns the column with the ParentCatalog property. | |
# For key columns, specify the name of the related column in the related table with the RelatedColumn property. | |
# For index columns, specify whether the sort order is ascending or descending with the SortOrder property. | |
# Access provider-specific properties with the Properties collection. | |
column_name = ole_column.Name | |
# Problematic Types in JetSQL -> Types in MySQL | |
# COUNTER -> Integer AUTOINCREMENT | |
# CURRENCY -> Decimal(8,2) | |
# Proof-of-concept: The .Type contains the type, but do we need to translate the type? | |
type = type_constant_for(ole_column.Type) # -> Returns a number. How do we match them up? | |
# Proof-of-concept: The .Attributes contains a constant or something -- is this given to us as a string? | |
is_nullable = ole_column.Attributes == 'adColNullable' ? true : false | |
# Need to gather the length / precision / scale, etc from Access too. | |
# if field[:ColType] =~ /numeric|decimal/i | |
# type = "#{field[:ColType]}(#{field[:numeric_precision]},#{field[:numeric_scale]})" | |
# else | |
# type = "#{field[:ColType]}(#{field[:Length]})" | |
# end | |
# Does Access have an IsIdentity feature? | |
# is_identity = field[:IsIdentity] == 1 | |
columns << MsAccessColumn.new(column_name, self.default_value_for_column(ole_column), ole_column, self.ole_table(table_name), type, is_nullable) | |
end | |
columns | |
end | |
def columns_hash(table_name) | |
chash = {} | |
self.columns.each do |column| | |
chash[column.name] = column | |
end | |
chash | |
end | |
def prefetch_primary_key?(table_name=nil) | |
self.columns(table_name).each do |col| | |
if col.property_value('Autoincrement') | |
return false | |
end | |
end | |
return true | |
end | |
def table_has_numeric_primary_key?(table_name) | |
has_numeric_primary_key = false | |
self.columns.each do |column| | |
# has_numeric_primary_key = true | |
end | |
has_numeric_primary_key | |
end | |
def default_sequence_name(table_name, primary_key) # :nodoc: | |
"#{table_name} #{primary_key}" | |
end | |
def next_sequence_value(sequence_name) #sequence_name will be like #{table_name}_seq | |
table_name, primary_key = sequence_name.split(' ') | |
self.columns(table_name).each do |col| | |
if col.property_value('Autoincrement') | |
return col.property_value('Seed') #Not completely sure if this part works properly yet! | |
end | |
end | |
#At this point, there was no autoincrement field. Sometimes the primary key field is not set to autoincrement, but the software is intended to do that. Therefore, we can do a select query to get that value. | |
return self.select_value("SELECT TOP 1 #{primary_key} AS single_value FROM [#{table_name}] ORDER BY #{primary_key} DESC").to_i+1 | |
end | |
def default_value_for_column(ole_column) | |
# if self.primary | |
# get_next_autoincrement_number(table_name) | |
# else | |
self.column_property_value(ole_column, 'Default') | |
# end | |
end | |
def column_property_value(ole_column, prop_name) | |
ole_column.Properties.each do |prop| | |
return unquote_property_value(prop.Value) if prop.Name == prop_name | |
end | |
end | |
# fix for MsAccess! | |
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) | |
execute(sql, name) | |
id_value || select_one("SELECT @@IDENTITY AS Ident")["Ident"] | |
end | |
def update(sql, name = nil) | |
execute(sql, name) do |handle| | |
handle.rows | |
end || select_one("SELECT @@ROWCOUNT AS AffectedRows")["AffectedRows"] | |
# fix select_one part for MsAccess! | |
end | |
alias_method :delete, :update | |
def execute(sql, name = nil) | |
# Take out identity_insert stuff for MsAccess | |
log(sql, name) do | |
@connection.Execute(sql) do |handle| | |
yield(handle) if block_given? | |
end | |
end | |
end | |
def begin_db_transaction | |
@connection.BeginTrans | |
rescue Exception | |
# Transactions aren't supported | |
end | |
def commit_db_transaction | |
@connection.CommitTrans | |
rescue Exception | |
# Transactions aren't supported | |
end | |
def rollback_db_transaction | |
@connection.RollbackTrans | |
rescue Exception | |
# Transactions aren't supported | |
end | |
def quote(value, column = nil) | |
return value.quoted_id if value.respond_to?(:quoted_id) | |
case value | |
when TrueClass then '1' | |
when FalseClass then '0' | |
else | |
if value && value.respond_to?(:acts_like) && value.respond_to?(:strftime) && value.acts_like?(:time) | |
"##{value.strftime("%Y/%m/%d %H:%M:%S")}#" | |
elsif value && value.respond_to?(:acts_like) && value.respond_to?(:strftime) && value.acts_like?(:date) | |
"##{value.strftime("%Y/%m/%d")}#" | |
else | |
super | |
end | |
end | |
end | |
def quote_string(string) | |
string.gsub(/\'/, "''") | |
end | |
def quote_column_name(name) | |
"[#{name}]" | |
end | |
# fix for MsAccess! | |
def add_limit_offset!(sql, options) | |
if options[:limit] and options[:offset] | |
total_rows = @connection.select_all("SELECT count(*) as TotalRows from (#{sql.gsub(/\bSELECT(\s+DISTINCT)?\b/i, "SELECT#{$1} TOP 1000000000")}) tally")[0][:TotalRows].to_i | |
if (options[:limit] + options[:offset]) >= total_rows | |
options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0 | |
end | |
sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT#{$1} TOP #{options[:limit] + options[:offset]} ") | |
sql << ") AS tmp1" | |
if options[:order] | |
options[:order] = options[:order].split(',').map do |field| | |
parts = field.split(" ") | |
tc = parts[0] | |
if sql =~ /\.\[/ and tc =~ /\./ # if column quoting used in query | |
tc.gsub!(/\./, '\\.\\[') | |
tc << '\\]' | |
end | |
if sql =~ /#{tc} AS (t\d_r\d\d?)/ | |
parts[0] = $1 | |
elsif parts[0] =~ /\w+\.(\w+)/ | |
parts[0] = $1 | |
end | |
parts.join(' ') | |
end.join(', ') | |
sql << " ORDER BY #{change_order_direction(options[:order])}) AS tmp2 ORDER BY #{options[:order]}" | |
else | |
sql << " ) AS tmp2" | |
end | |
elsif sql !~ /^\s*SELECT (@@|COUNT\()/i | |
sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i) do | |
"SELECT#{$1} TOP #{options[:limit]}" | |
end unless options[:limit].nil? | |
end | |
end | |
def add_limit_without_offset!(sql, limit) | |
limit.nil? ? sql : sql.gsub!(/SELECT/i, "SELECT TOP #{limit}") | |
end | |
def recreate_database(name) | |
drop_database(name) | |
create_database(name) | |
end | |
def drop_database(name) | |
execute "DROP DATABASE #{name}" | |
end | |
def create_database(name) | |
execute "CREATE DATABASE #{name}" | |
end | |
# fix for MsAccess! | |
def current_database | |
@connection.select_one("select DB_NAME()")[0] | |
end | |
def type_constant_for(type_int) | |
{ | |
20 => 'adBigInt', | |
128 => 'adBinary', | |
11 => 'adBoolean', | |
8 => 'adBSTR', | |
136 => 'adChapter', | |
129 => 'adChar', | |
6 => 'adCurrency', | |
7 => 'adDate', | |
133 => 'adDBDate', | |
134 => 'adDBTime', | |
14 => 'adDecimal', | |
5 => 'adDouble', | |
0 => 'adEmpty', | |
10 => 'adError', | |
64 => 'adFileTime', | |
72 => 'adGUID', | |
3 => 'adInteger', | |
205 => 'adLongVarBinary', | |
201 => 'adLongVarChar', | |
203 => 'adLongVarWChar', | |
131 => 'adNumeric', | |
138 => 'adPropVariant', | |
4 => 'adSingle', | |
2 => 'adSmallInt', | |
16 => 'adTinyInt', | |
21 => 'adUnsignedBigInt', | |
19 => 'adUnsignedInt', | |
18 => 'adUnsignedSmallInt', | |
17 => 'adUnsignedTinyInt', | |
132 => 'adUserDefined', | |
204 => 'adVarBinary', | |
200 => 'adVarChar', | |
12 => 'adVariant', | |
139 => 'adVarNumeric', | |
202 => 'adVarWChar', | |
130 => 'adWChar' | |
}[type_int] | |
end | |
# fix for MsAccess! | |
def indexes(table_name, name = nil) | |
ActiveRecord::Base.connection.instance_variable_get("@connection")["AutoCommit"] = false | |
indexes = [] | |
execute("EXEC sp_helpindex '#{table_name}'", name) do |handle| | |
if handle.column_info.any? | |
handle.each do |index| | |
unique = index[1] =~ /unique/ | |
primary = index[1] =~ /primary key/ | |
if !primary | |
indexes << IndexDefinition.new(table_name, index[0], unique, index[2].split(", ").map {|e| e.gsub('(-)','')}) | |
end | |
end | |
end | |
end | |
indexes | |
ensure | |
ActiveRecord::Base.connection.instance_variable_get("@connection")["AutoCommit"] = true | |
end | |
def ole_table(table_name) | |
self.catalog.Tables.each do |t| | |
return t if t.Name == table_name | |
end | |
return nil | |
end | |
def rename_table(name, new_name) | |
ole_table(name.to_s).Name = new_name.to_s | |
end | |
# Adds a new column to the named table. | |
# See TableDefinition#column for details of the options you can use. | |
def add_column(table_name, column_name, type, options = {}) | |
add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" | |
add_column_options!(add_column_sql, options) | |
# TODO: Add support to mimic date columns, using constraints to mark them as such in the database | |
# add_column_sql << " CONSTRAINT ck__#{table_name}__#{column_name}__date_only CHECK ( CONVERT(CHAR(12), #{quote_column_name(column_name)}, 14)='00:00:00:000' )" if type == :date | |
execute(add_column_sql) | |
end | |
def rename_column(table, column, new_column_name) | |
execute "EXEC sp_rename '#{table}.#{column}', '#{new_column_name}'" | |
end | |
def change_column(table_name, column_name, type, options = {}) #:nodoc: | |
sql_commands = ["ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"] | |
if options_include_default?(options) | |
remove_default_constraint(table_name, column_name) | |
sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(options[:default], options[:column])} FOR #{column_name}" | |
end | |
sql_commands.each {|c| | |
execute(c) | |
} | |
end | |
def change_column_default(table_name, column_name, default) | |
remove_default_constraint(table_name, column_name) | |
execute "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(default, column_name)} FOR #{column_name}" | |
end | |
def remove_column(table_name, column_name) | |
execute "ALTER TABLE [#{table_name}] DROP COLUMN [#{column_name}]" | |
end | |
def remove_default_constraint(table_name, column_name) | |
constraints = select "select def.name from sysobjects def, syscolumns col, sysobjects tab where col.cdefault = def.id and col.name = '#{column_name}' and tab.name = '#{table_name}' and col.id = tab.id" | |
constraints.each do |constraint| | |
execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["name"]}" | |
end | |
end | |
def remove_check_constraints(table_name, column_name) | |
# TODO remove all constraints in single method | |
constraints = select "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{table_name}' and COLUMN_NAME = '#{column_name}'" | |
constraints.each do |constraint| | |
execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["CONSTRAINT_NAME"]}" | |
end | |
end | |
def remove_index(table_name, options = {}) | |
execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}" | |
end | |
# def select_value(sql, name=nil) | |
# puts "Executing SQL \"#{sql}\", looking for #{name}" | |
# records = select(sql, name) | |
# return records[0][name] | |
# end | |
private | |
def select(sql, name=nil) | |
repair_special_columns(sql) | |
recordset = WIN32OLE.new('ADODB.Recordset') | |
recordset.Open(sql, @connection) # Fails for some databases: why? | |
recordset | |
rows = [] | |
recordset.GetRows.each do |row| | |
rows << row | |
end unless recordset.EOF | |
records = rows.transpose | |
result = [] | |
records.each do |row| | |
row_hash = {} | |
row.each_with_index do |value, i| | |
row_hash[field_names_from_recordset(recordset)[i]] = value | |
end | |
result << row_hash | |
end | |
result | |
end | |
# def select_one(sql, name=nil) | |
# result = select(sql, name) | |
# result.first if result | |
# end | |
def unquote_property_value(value) | |
if value =~ /\\?["'](.+)\\?["']/ | |
value = $1 | |
end | |
value | |
end | |
def field_names_from_recordset(recordset) | |
fields = [] | |
recordset.Fields.each do |field| | |
fields << field.Name # + "("+self.type_constant_for(field.Type)+")" | |
end | |
return fields | |
end | |
# Turns IDENTITY_INSERT ON for table during execution of the block | |
# N.B. This sets the state of IDENTITY_INSERT to OFF after the | |
# block has been executed without regard to its previous state | |
def with_identity_insert_enabled(table_name, &block) | |
set_identity_insert(table_name, true) | |
yield | |
ensure | |
set_identity_insert(table_name, false) | |
end | |
def set_identity_insert(table_name, enable = true) | |
execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" | |
rescue Exception => e | |
raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}" | |
end | |
def get_table_name(sql) | |
if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i | |
$1 | |
elsif sql =~ /from\s+([^\(\s]+)\s*/i | |
$1 | |
else | |
nil | |
end | |
end | |
def identity_column(table_name) | |
@table_columns = {} unless @table_columns | |
@table_columns[table_name] = columns(table_name) if @table_columns[table_name] == nil | |
@table_columns[table_name].each do |col| | |
return col.name if col.identity | |
end | |
return nil | |
end | |
def query_requires_identity_insert?(sql) | |
table_name = get_table_name(sql) | |
id_column = identity_column(table_name) | |
sql =~ /\[#{id_column}\]/ ? table_name : nil | |
end | |
def change_order_direction(order) | |
order.split(",").collect {|fragment| | |
case fragment | |
when /\bDESC\b/i then fragment.gsub(/\bDESC\b/i, "ASC") | |
when /\bASC\b/i then fragment.gsub(/\bASC\b/i, "DESC") | |
else String.new(fragment).split(',').join(' DESC,') + ' DESC' | |
end | |
}.join(",") | |
end | |
def get_special_columns(table_name) | |
special = [] | |
@table_columns ||= {} | |
@table_columns[table_name] ||= columns(table_name) | |
@table_columns[table_name].each do |col| | |
special << col.name if col.is_special | |
end | |
special | |
end | |
def repair_special_columns(sql) | |
special_cols = get_special_columns(get_table_name(sql)) | |
for col in special_cols.to_a | |
sql.gsub!(Regexp.new(" #{col.to_s} = "), " #{col.to_s} LIKE ") | |
sql.gsub!(/ORDER BY #{col.to_s}/i, '') | |
end | |
sql | |
end | |
end #class MsAccessAdapter < AbstractAdapter | |
end #module ConnectionAdapters | |
end #module ActiveRecord |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment