mssqlclient 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ -- 0.1.0:
2
+ * Initial Gem Release
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+ require 'rake/rdoctask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/contrib/rubyforgepublisher'
8
+ require 'pscp'
9
+
10
+ PACKAGE_VERSION = '0.1.0'
11
+
12
+ PACKAGE_FILES = FileList[
13
+ 'README',
14
+ 'CHANGELOG',
15
+ 'RAKEFILE',
16
+ 'lib/**/*.{rb,dll}',
17
+ 'test/*.rb',
18
+ 'src/*.{cpp,h,ico,rc,sln,vcproj}'
19
+ ].to_a
20
+
21
+ PROJECT = 'mssqlclient'
22
+
23
+ ENV['RUBYFORGE_USER'] = "ssmoot@rubyforge.org"
24
+ ENV['RUBYFORGE_PROJECT'] = "/var/www/gforge-projects/#{PROJECT}"
25
+
26
+ task :default => [:rdoc]
27
+
28
+ desc 'Generate Documentation'
29
+ rd = Rake::RDocTask.new do |rdoc|
30
+ rdoc.rdoc_dir = 'doc'
31
+ rdoc.title = "MsSqlClient -- A native Win32/ADO.NET ActiveRecord Adapter for Microsoft's SQL Server"
32
+ rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README'
33
+ rdoc.rdoc_files.include(PACKAGE_FILES)
34
+ end
35
+
36
+ gem_spec = Gem::Specification.new do |s|
37
+ s.platform = Gem::Platform::RUBY
38
+ s.name = PROJECT
39
+ s.summary = "A native Win32/ADO.NET ActiveRecord Adapter for Microsoft's SQL Server"
40
+ s.description = "A faster, better way to integrate ActiveRecord with MS SQL Server"
41
+ s.version = PACKAGE_VERSION
42
+
43
+ s.authors = 'Sam Smoot', 'Scott Bauer'
44
+ s.email = 'ssmoot@gmail.com; bauer.mail@gmail.com'
45
+ s.rubyforge_project = PROJECT
46
+ s.homepage = 'http://substantiality.net'
47
+
48
+ s.files = PACKAGE_FILES
49
+
50
+ s.require_path = 'lib'
51
+ s.requirements << 'active_record'
52
+ s.autorequire = 'mssqlclient_adapter'
53
+
54
+ s.has_rdoc = true
55
+ s.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README'
56
+ s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$/ }.to_a
57
+ end
58
+
59
+ Rake::GemPackageTask.new(gem_spec) do |p|
60
+ p.gem_spec = gem_spec
61
+ p.need_tar = true
62
+ p.need_zip = true
63
+ end
64
+
65
+ desc "Publish RDOC to RubyForge"
66
+ task :rubyforge => [:rdoc, :gem] do
67
+ Rake::SshDirPublisher.new(ENV['RUBYFORGE_USER'], ENV['RUBYFORGE_PROJECT'], 'doc').upload
68
+ end
data/README ADDED
@@ -0,0 +1 @@
1
+ This is the README! Interesting!
@@ -0,0 +1,92 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class ColumnWithIdentityAndOrdinal < Column# :nodoc:
4
+ attr_reader :identity, :is_special, :scale
5
+
6
+ def initialize(name, default, sql_type = nil, is_identity = false, null = true, scale_value = 0, ordinal = 0)
7
+ super(name, default, sql_type, null)
8
+ @identity = is_identity
9
+ @is_special = sql_type =~ /text|ntext|image/i ? true : false
10
+ @scale = scale_value
11
+ # SQL Server only supports limits on *char and float types
12
+ @limit = nil unless @type == :float or @type == :string
13
+ @ordinal = ordinal
14
+ end
15
+
16
+ def simplified_type(field_type)
17
+ case field_type
18
+ when /int|bigint|smallint|tinyint/i then :integer
19
+ when /float|double|decimal|money|numeric|real|smallmoney/i then @scale == 0 ? :integer : :float
20
+ when /datetime|smalldatetime/i then :datetime
21
+ when /timestamp/i then :timestamp
22
+ when /time/i then :time
23
+ when /text|ntext/i then :text
24
+ when /binary|image|varbinary/i then :binary
25
+ when /char|nchar|nvarchar|string|varchar/i then :string
26
+ when /bit/i then :boolean
27
+ when /uniqueidentifier/i then :string
28
+ end
29
+ end
30
+
31
+ def type_cast(value)
32
+ return nil if value.nil? || value =~ /^\s*null\s*$/i
33
+ case type
34
+ when :string then value
35
+ when :integer then value == true || value == false ? value == true ? 1 : 0 : value.to_i
36
+ when :float then value.to_f
37
+ when :datetime then cast_to_datetime(value)
38
+ when :timestamp then cast_to_time(value)
39
+ when :time then cast_to_time(value)
40
+ when :date then cast_to_datetime(value)
41
+ when :boolean then value == true or (value =~ /^t(rue)?$/i) == 0 or value.to_s == '1'
42
+ else value
43
+ end
44
+ end
45
+
46
+ def cast_to_time(value)
47
+ return value if value.is_a?(Time)
48
+ time_array = ParseDate.parsedate(value)
49
+ time_array[0] ||= 2000
50
+ time_array[1] ||= 1
51
+ time_array[2] ||= 1
52
+ Time.send(Base.default_timezone, *time_array) rescue nil
53
+ end
54
+
55
+ def cast_to_datetime(value)
56
+ if value.is_a?(Time)
57
+ if value.year != 0 and value.month != 0 and value.day != 0
58
+ return value
59
+ else
60
+ return Time.mktime(2000, 1, 1, value.hour, value.min, value.sec) rescue nil
61
+ end
62
+ end
63
+ return cast_to_time(value) if value.is_a?(Date) or value.is_a?(String) rescue nil
64
+ value
65
+ end
66
+
67
+ # These methods will only allow the adapter to insert binary data with a length of 7K or less
68
+ # because of a SQL Server statement length policy.
69
+ def self.string_to_binary(value)
70
+ value.gsub(/(\r|\n|\0|\x1a)/) do
71
+ case $1
72
+ when "\r" then "%00"
73
+ when "\n" then "%01"
74
+ when "\0" then "%02"
75
+ when "\x1a" then "%03"
76
+ end
77
+ end
78
+ end
79
+
80
+ def self.binary_to_string(value)
81
+ value.gsub(/(%00|%01|%02|%03)/) do
82
+ case $1
83
+ when "%00" then "\r"
84
+ when "%01" then "\n"
85
+ when "%02\0" then "\0"
86
+ when "%03" then "\x1a"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
Binary file
@@ -0,0 +1 @@
1
+ require 'mssqlclient_adapter'
@@ -0,0 +1,391 @@
1
+ require 'active_record' # 'active_record/connection_adapters/abstract_adapter'
2
+ RAILS_CONNECTION_ADAPTERS << 'mssqlclient'
3
+
4
+ require 'bigdecimal'
5
+ require 'ms_sql_client'
6
+ require 'column_with_identity_and_ordinal'
7
+
8
+ module ActiveRecord
9
+ class Base
10
+ def self.mssqlclient_connection(config) #:nodoc:
11
+ ConnectionAdapters::MsSqlClientAdapter.new(logger, config.symbolize_keys)
12
+ end
13
+ end # class Base
14
+
15
+ module ConnectionAdapters
16
+ class MsSqlClientAdapter < AbstractAdapter
17
+
18
+ include MsSqlClient
19
+
20
+ def initialize(logger, connection_options = nil)
21
+ @connection_options, @logger = connection_options, logger
22
+ @connection = self
23
+ @runtime = @last_verification = 0
24
+ @active = true
25
+ @connection_options[:schema] ||= 'dbo'
26
+ end
27
+
28
+ def connection_string
29
+ if @connection_options[:trusted]
30
+ "Server=#{@connection_options[:host]};Database=#{@connection_options[:database]};Trusted_Connection=True;"
31
+ elsif @connection_options[:connection_string]
32
+ @connection_options[:connection_string]
33
+ else
34
+ "Data Source=#{@connection_options[:host]};Initial Catalog=#{@connection_options[:database]};User Id=#{@connection_options[:username]};Password=#{@connection_options[:password]};";
35
+ end
36
+ end
37
+
38
+ def native_database_types
39
+ {
40
+ :primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
41
+ :string => { :name => "varchar", :limit => 255 },
42
+ :text => { :name => "text" },
43
+ :integer => { :name => "int" },
44
+ :float => { :name => "float", :limit => 8 },
45
+ :datetime => { :name => "datetime" },
46
+ :timestamp => { :name => "datetime" },
47
+ :time => { :name => "datetime" },
48
+ :date => { :name => "datetime" },
49
+ :binary => { :name => "image"},
50
+ :boolean => { :name => "bit"}
51
+ }
52
+ end
53
+
54
+ def adapter_name
55
+ 'MsSqlClient'
56
+ end
57
+
58
+ def supports_migrations? #:nodoc:
59
+ true
60
+ end
61
+
62
+ # CONNECTION MANAGEMENT ====================================#
63
+
64
+ def verify!(timeout)
65
+ @last_verification = Time.now.to_i
66
+ end
67
+
68
+ # Returns true if the connection is active.
69
+ def active?
70
+ @active
71
+ end
72
+
73
+ # Reconnects to the database, returns false if no connection could be made.
74
+ def reconnect!
75
+ @active = true
76
+ end
77
+
78
+ # Disconnects from the database
79
+
80
+ def disconnect!
81
+ @active = false
82
+ true
83
+ end
84
+
85
+ def select_all(sql, name=nil)
86
+ select(sql)
87
+ end
88
+
89
+ #alias_method :select_all, :select
90
+
91
+ def select_one(sql, name = nil)
92
+ add_limit!(sql, :limit => 1)
93
+ result = select(sql, name)
94
+ result.nil? ? nil : result.first
95
+ end
96
+
97
+ def columns(table_name, name = nil)
98
+ return [] if table_name.blank?
99
+ table_name = table_name.to_s if table_name.is_a?(Symbol)
100
+ table_name = table_name.split('.')[-1] unless table_name.nil?
101
+ sql = "SELECT COLUMN_NAME as ColName, COLUMN_DEFAULT as DefaultValue, DATA_TYPE as ColType, IS_NULLABLE as IsNullable, COL_LENGTH('#{table_name}', COLUMN_NAME) as Length, COLUMNPROPERTY(OBJECT_ID('#{table_name}'), COLUMN_NAME, 'IsIdentity') as IsIdentity, NUMERIC_SCALE as Scale FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '#{table_name}' AND TABLE_SCHEMA = '#{@connection_options[:schema]}'"
102
+ # puts sql
103
+ # Comment out if you want to have the Columns select statment logged.
104
+ # Personnally, I think it adds unneccessary bloat to the log.
105
+ # If you do comment it out, make sure to un-comment the "result" line that follows
106
+ result = log(sql, name) { _select(sql) }
107
+ #result = @connection.select_all(sql)
108
+ columns = []
109
+ result.each { |field| field.symbolize_keys!; columns << ColumnWithIdentityAndOrdinal.new(field[:ColName], field[:DefaultValue].to_s.gsub!(/[()\']/,"") =~ /null/ ? nil : field[:DefaultValue], "#{field[:ColType]}(#{field[:Length]})", field[:IsIdentity] == 1 ? true : false, field[:IsNullable] == 'YES', field[:Scale], field[:Ordinal]) }
110
+ # puts columns.inspect
111
+ columns
112
+ end
113
+
114
+ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
115
+ table_name = get_table_name(sql)
116
+ col = get_identity_column(table_name)
117
+ ii_enabled = false
118
+
119
+ if col != nil
120
+ if query_contains_identity_column(sql, col)
121
+ sql = wrap_identity_insert(table_name, sql)
122
+ end
123
+ end
124
+
125
+ log(sql, name) do
126
+ new_id = _insert(sql)
127
+ id_value || new_id
128
+ end
129
+ end
130
+
131
+ def execute(sql, name = nil)
132
+ if sql =~ /^\s*INSERT/i
133
+ insert(sql, name)
134
+ else
135
+ # puts "About to execute"
136
+ # sleep 1
137
+ log(sql, name) { _execute(sql) }
138
+ end
139
+ end
140
+
141
+ def update(sql, name = nil)
142
+ execute(sql, name)
143
+ end
144
+ alias_method :delete, :update
145
+
146
+ def quote(value, column = nil)
147
+ case value
148
+ when String
149
+ if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
150
+ "'#{quote_string(column.class.string_to_binary(value))}'"
151
+ else
152
+ "N'#{quote_string(value)}'"
153
+ end
154
+ when NilClass then "NULL"
155
+ when TrueClass then '1'
156
+ when FalseClass then '0'
157
+ when Float, Fixnum, Bignum then value.to_s
158
+ when Date then "'#{value.to_s}'"
159
+ when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
160
+ else "'#{quote_string(value.to_yaml)}'"
161
+ end
162
+ end
163
+
164
+ def quote_string(string)
165
+ string.gsub(/\'/, "''")
166
+ end
167
+
168
+ def quoted_true
169
+ "1"
170
+ end
171
+
172
+ def quoted_false
173
+ "0"
174
+ end
175
+
176
+ def quote_column_name(name)
177
+ "[#{name}]"
178
+ end
179
+
180
+ def add_limit_offset!(sql, options)
181
+ # STDOUT << "\n\nadd_limit_offset options = #{options.inspect}\n\n"
182
+ # STDOUT << "add_limit_offset sql = #{sql}\n\n"
183
+
184
+ if options[:limit] and options[:offset]
185
+ sub_query = sql.gsub /\bSELECT\b(\s*DISTINCT)?/i do
186
+ "SELECT #{$1} TOP 1000000000"
187
+ end
188
+
189
+ total_rows = select("SELECT count(*) as TotalRows from (#{sub_query}) tally")[0]['TotalRows'].to_i
190
+
191
+ if (options[:limit] + options[:offset]) >= total_rows
192
+ options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0
193
+ end
194
+ sql.sub!(/^\s*SELECT/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT TOP #{options[:limit] + options[:offset]} ")
195
+ sql << ") AS tmp1"
196
+
197
+ # (SELECT TOP 0 * FROM (SELECT TOP 10 DISTINCT posts.id
198
+
199
+ sql.sub! /\(SELECT\sTOP\s(\d+)\s+DISTINCT/ do
200
+ "(SELECT DISTINCT TOP #{$1} "
201
+ end
202
+
203
+ if options[:order]
204
+ options[:order] = options[:order].split(',').map do |field|
205
+ parts = field.split(" ")
206
+ tc = parts[0]
207
+ if sql =~ /\.\[/ and tc =~ /\./ # if column quoting used in query
208
+ tc.gsub!(/\./, '\\.\\[')
209
+ tc << '\\]'
210
+ end
211
+ if sql =~ /#{tc} AS (t\d_r\d\d?)/
212
+ parts[0] = $1
213
+ end
214
+ parts.join(' ')
215
+ end.join(', ')
216
+ sql << " ORDER BY #{change_order_direction(options[:order])}) AS tmp2 ORDER BY #{options[:order]}"
217
+ else
218
+ sql << " ) AS tmp2"
219
+ end
220
+ elsif sql !~ /^\s*SELECT (@@|COUNT\()/i
221
+ sql.sub!(/^\s*SELECT([\s]*distinct)?/i) do
222
+ "SELECT#{$1} TOP #{options[:limit]}"
223
+ end unless options[:limit].nil?
224
+ end
225
+ end
226
+
227
+ def recreate_database(name)
228
+ drop_database(name)
229
+ create_database(name)
230
+ end
231
+
232
+ def drop_database(name)
233
+ execute "DROP DATABASE #{name}"
234
+ end
235
+
236
+ def create_database(name)
237
+ execute "CREATE DATABASE #{name}"
238
+ end
239
+
240
+ def current_database
241
+ @connection.select_one("select DB_NAME() as [Name]")['Name']
242
+ end
243
+
244
+ def tables(name = nil)
245
+ results = select("SELECT table_name from information_schema.tables WHERE table_type = 'BASE TABLE'", name)
246
+
247
+ results.inject([]) do |tables, field|
248
+ table_name = field['table_name']
249
+ tables << table_name unless table_name == 'dtproperties'
250
+ tables
251
+ end
252
+ end
253
+
254
+ def indexes(table_name, name = nil)
255
+ indexes = []
256
+ select("EXEC sp_helpindex #{table_name}", name).each do |index|
257
+ unique = index['index_description'] =~ /unique/
258
+ primary = index['index_description'] =~ /primary key/
259
+ if !primary
260
+ indexes << IndexDefinition.new(table_name, index['index_name'], unique, index['index_keys'].split(", "))
261
+ end
262
+ end
263
+ indexes
264
+ end
265
+
266
+ def rename_table(name, new_name)
267
+ execute "EXEC sp_rename '#{name}', '#{new_name}'"
268
+ end
269
+
270
+ def remove_column(table_name, column_name)
271
+ execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name}"
272
+ end
273
+
274
+ def rename_column(table, column, new_column_name)
275
+ execute "EXEC sp_rename '#{table}.#{column}', '#{new_column_name}'"
276
+ end
277
+
278
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
279
+ sql_commands = ["ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit])}"]
280
+ if options[:default]
281
+ remove_default_constraint(table_name, column_name)
282
+ sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{options[:default]} FOR #{column_name}"
283
+ end
284
+ sql_commands.each {|c|
285
+ execute(c)
286
+ }
287
+ end
288
+
289
+ def remove_column(table_name, column_name)
290
+ remove_default_constraint(table_name, column_name)
291
+ execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name}"
292
+ end
293
+
294
+ def remove_default_constraint(table_name, column_name)
295
+ defaults = 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"
296
+ defaults.each {|constraint|
297
+ execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["name"]}"
298
+ }
299
+ end
300
+
301
+ def remove_index(table_name, options = {})
302
+ execute "DROP INDEX #{table_name}.[#{index_name(table_name, options)}]"
303
+ end
304
+
305
+ def type_to_sql(type, limit = nil) #:nodoc:
306
+ native = native_database_types[type]
307
+ # if there's no :limit in the default type definition, assume that type doesn't support limits
308
+ limit = limit || native[:limit]
309
+ column_type_sql = native[:name]
310
+ column_type_sql << "(#{limit})" if limit
311
+ column_type_sql
312
+ end
313
+
314
+ private
315
+ def select(sql, name = nil)
316
+ repair_special_columns(sql)
317
+ log(sql, name) do
318
+ _select(sql)
319
+ end
320
+ end
321
+
322
+ def wrap_identity_insert(table_name, sql)
323
+ if has_identity_column(table_name)
324
+ <<-SQL
325
+ SET IDENTITY_INSERT #{table_name} ON
326
+ #{sql}
327
+ SET IDENTITY_INSERT #{table_name} OFF
328
+ SQL
329
+ end
330
+ end
331
+
332
+ def get_table_name(sql)
333
+ if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
334
+ $1
335
+ elsif sql =~ /from\s+([^\(\s]+)\s*/i
336
+ $1
337
+ else
338
+ nil
339
+ end
340
+ end
341
+
342
+ def has_identity_column(table_name)
343
+ !get_identity_column(table_name).nil?
344
+ end
345
+
346
+ def get_identity_column(table_name)
347
+ @table_columns = {} unless @table_columns
348
+ @table_columns[table_name] = columns(table_name) if @table_columns[table_name] == nil
349
+ @table_columns[table_name].each do |col|
350
+ return col.name if col.identity
351
+ end
352
+
353
+ return nil
354
+ end
355
+
356
+ def query_contains_identity_column(sql, col)
357
+ sql =~ /\[#{col}\]/
358
+ end
359
+
360
+ def change_order_direction(order)
361
+ order.split(",").collect {|fragment|
362
+ case fragment
363
+ when /\bDESC\b/i then fragment.gsub(/\bDESC\b/i, "ASC")
364
+ when /\bASC\b/i then fragment.gsub(/\bASC\b/i, "DESC")
365
+ else String.new(fragment).split(',').join(' DESC,') + ' DESC'
366
+ end
367
+ }.join(",")
368
+ end
369
+
370
+ def get_special_columns(table_name)
371
+ special = []
372
+ @table_columns ||= {}
373
+ @table_columns[table_name] ||= columns(table_name)
374
+ @table_columns[table_name].each do |col|
375
+ special << col.name if col.is_special
376
+ end
377
+ special
378
+ end
379
+
380
+ def repair_special_columns(sql)
381
+ special_cols = get_special_columns(get_table_name(sql))
382
+ for col in special_cols.to_a
383
+ sql.gsub!(Regexp.new(" #{col.to_s} = "), " #{col.to_s} LIKE ")
384
+ sql.gsub!(/ORDER BY #{col.to_s}/i, '')
385
+ end
386
+ sql
387
+ end
388
+
389
+ end #class MsSqlClientAdapter < AbstractAdapter
390
+ end #module ConnectionAdapters
391
+ end #module ActiveRecord