mssqlclient 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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