activerecord-sqlserver-adapter 2.3.7 → 3.2.18

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +385 -61
  3. data/MIT-LICENSE +1 -1
  4. data/VERSION +1 -0
  5. data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +42 -0
  6. data/lib/active_record/connection_adapters/sqlserver/core_ext/database_statements.rb +97 -0
  7. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +41 -0
  8. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +26 -0
  9. data/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb +38 -0
  10. data/lib/active_record/connection_adapters/sqlserver/core_ext/relation.rb +19 -0
  11. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +49 -0
  12. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +458 -0
  13. data/lib/active_record/connection_adapters/sqlserver/errors.rb +36 -0
  14. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +113 -0
  15. data/lib/active_record/connection_adapters/sqlserver/schema_cache.rb +85 -0
  16. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +376 -0
  17. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_table.rb +69 -0
  18. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_xml.rb +25 -0
  19. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +67 -0
  20. data/lib/active_record/connection_adapters/sqlserver/utils.rb +32 -0
  21. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +344 -1055
  22. data/lib/arel/visitors/sqlserver.rb +389 -0
  23. metadata +60 -83
  24. data/README.rdoc +0 -190
  25. data/RUNNING_UNIT_TESTS +0 -65
  26. data/Rakefile +0 -41
  27. data/autotest/discover.rb +0 -4
  28. data/autotest/railssqlserver.rb +0 -16
  29. data/autotest/sqlserver.rb +0 -54
  30. data/lib/active_record/connection_adapters/sqlserver_adapter/core_ext/active_record.rb +0 -151
  31. data/lib/active_record/connection_adapters/sqlserver_adapter/core_ext/odbc.rb +0 -40
  32. data/test/cases/aaaa_create_tables_test_sqlserver.rb +0 -19
  33. data/test/cases/adapter_test_sqlserver.rb +0 -756
  34. data/test/cases/attribute_methods_test_sqlserver.rb +0 -33
  35. data/test/cases/basics_test_sqlserver.rb +0 -21
  36. data/test/cases/calculations_test_sqlserver.rb +0 -20
  37. data/test/cases/column_test_sqlserver.rb +0 -285
  38. data/test/cases/connection_test_sqlserver.rb +0 -146
  39. data/test/cases/eager_association_test_sqlserver.rb +0 -42
  40. data/test/cases/execute_procedure_test_sqlserver.rb +0 -44
  41. data/test/cases/inheritance_test_sqlserver.rb +0 -28
  42. data/test/cases/method_scoping_test_sqlserver.rb +0 -28
  43. data/test/cases/migration_test_sqlserver.rb +0 -123
  44. data/test/cases/named_scope_test_sqlserver.rb +0 -21
  45. data/test/cases/offset_and_limit_test_sqlserver.rb +0 -108
  46. data/test/cases/pessimistic_locking_test_sqlserver.rb +0 -125
  47. data/test/cases/query_cache_test_sqlserver.rb +0 -24
  48. data/test/cases/schema_dumper_test_sqlserver.rb +0 -72
  49. data/test/cases/specific_schema_test_sqlserver.rb +0 -97
  50. data/test/cases/sqlserver_helper.rb +0 -127
  51. data/test/cases/table_name_test_sqlserver.rb +0 -38
  52. data/test/cases/transaction_test_sqlserver.rb +0 -93
  53. data/test/cases/unicode_test_sqlserver.rb +0 -50
  54. data/test/cases/validations_test_sqlserver.rb +0 -35
  55. data/test/connections/native_sqlserver/connection.rb +0 -25
  56. data/test/connections/native_sqlserver_odbc/connection.rb +0 -27
  57. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +0 -11
  58. data/test/schema/sqlserver_specific_schema.rb +0 -94
@@ -0,0 +1,376 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sqlserver
4
+ module SchemaStatements
5
+
6
+ def native_database_types
7
+ @native_database_types ||= initialize_native_database_types.freeze
8
+ end
9
+
10
+ def tables(table_type = 'BASE TABLE')
11
+ select_values "SELECT #{lowercase_schema_reflection_sql('TABLE_NAME')} FROM INFORMATION_SCHEMA.TABLES #{"WHERE TABLE_TYPE = '#{table_type}'" if table_type} ORDER BY TABLE_NAME", 'SCHEMA'
12
+ end
13
+
14
+ def table_exists?(table_name)
15
+ return false if table_name.blank?
16
+ unquoted_table_name = Utils.unqualify_table_name(table_name)
17
+ super || tables.include?(unquoted_table_name) || views.include?(unquoted_table_name)
18
+ end
19
+
20
+ def indexes(table_name, name = nil)
21
+ data = select("EXEC sp_helpindex #{quote(table_name)}",name) rescue []
22
+ data.inject([]) do |indexes,index|
23
+ index = index.with_indifferent_access
24
+ if index[:index_description] =~ /primary key/
25
+ indexes
26
+ else
27
+ name = index[:index_name]
28
+ unique = index[:index_description] =~ /unique/
29
+ columns = index[:index_keys].split(',').map do |column|
30
+ column.strip!
31
+ column.gsub! '(-)', '' if column.ends_with?('(-)')
32
+ column
33
+ end
34
+ indexes << IndexDefinition.new(table_name, name, unique, columns)
35
+ end
36
+ end
37
+ end
38
+
39
+ def columns(table_name, name = nil)
40
+ return [] if table_name.blank?
41
+ column_definitions(table_name).collect do |ci|
42
+ sqlserver_options = ci.except(:name,:default_value,:type,:null).merge(:database_year=>database_year)
43
+ SQLServerColumn.new ci[:name], ci[:default_value], ci[:type], ci[:null], sqlserver_options
44
+ end
45
+ end
46
+
47
+ def rename_table(table_name, new_name)
48
+ do_execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
49
+ end
50
+
51
+ def remove_column(table_name, *column_names)
52
+ raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
53
+ ActiveSupport::Deprecation.warn 'Passing array to remove_columns is deprecated, please use multiple arguments, like: `remove_columns(:posts, :foo, :bar)`', caller if column_names.flatten!
54
+ column_names.flatten.each do |column_name|
55
+ remove_check_constraints(table_name, column_name)
56
+ remove_default_constraint(table_name, column_name)
57
+ remove_indexes(table_name, column_name)
58
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
59
+ end
60
+ end
61
+
62
+ def change_column(table_name, column_name, type, options = {})
63
+ sql_commands = []
64
+ indexes = []
65
+ column_object = schema_cache.columns[table_name].detect { |c| c.name.to_s == column_name.to_s }
66
+
67
+ if options_include_default?(options) || (column_object && column_object.type != type.to_sym)
68
+ remove_default_constraint(table_name,column_name)
69
+ indexes = indexes(table_name).select{ |index| index.columns.include?(column_name.to_s) }
70
+ remove_indexes(table_name, column_name)
71
+ end
72
+ sql_commands << "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(options[:default])} WHERE #{quote_column_name(column_name)} IS NULL" if !options[:null].nil? && options[:null] == false && !options[:default].nil?
73
+ sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
74
+ sql_commands[-1] << " NOT NULL" if !options[:null].nil? && options[:null] == false
75
+ if options_include_default?(options)
76
+ sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name,column_name)} DEFAULT #{quote(options[:default])} FOR #{quote_column_name(column_name)}"
77
+ end
78
+
79
+ #Add any removed indexes back
80
+ indexes.each do |index|
81
+ sql_commands << "CREATE INDEX #{quote_table_name(index.name)} ON #{quote_table_name(table_name)} (#{index.columns.collect {|c|quote_column_name(c)}.join(', ')})"
82
+ end
83
+ sql_commands.each { |c| do_execute(c) }
84
+ end
85
+
86
+ def change_column_default(table_name, column_name, default)
87
+ remove_default_constraint(table_name, column_name)
88
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote(default)} FOR #{quote_column_name(column_name)}"
89
+ end
90
+
91
+ def rename_column(table_name, column_name, new_column_name)
92
+ schema_cache.clear_table_cache!(table_name)
93
+ detect_column_for! table_name, column_name
94
+ do_execute "EXEC sp_rename '#{table_name}.#{column_name}', '#{new_column_name}', 'COLUMN'"
95
+ end
96
+
97
+ def remove_index!(table_name, index_name)
98
+ do_execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
99
+ end
100
+
101
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
102
+ type_limitable = ['string','integer','float','char','nchar','varchar','nvarchar'].include?(type.to_s)
103
+ limit = nil unless type_limitable
104
+ case type.to_s
105
+ when 'integer'
106
+ case limit
107
+ when 1..2 then 'smallint'
108
+ when 3..4, nil then 'integer'
109
+ when 5..8 then 'bigint'
110
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
111
+ end
112
+ else
113
+ super
114
+ end
115
+ end
116
+
117
+ def change_column_null(table_name, column_name, allow_null, default = nil)
118
+ column = detect_column_for! table_name, column_name
119
+ if !allow_null.nil? && allow_null == false && !default.nil?
120
+ do_execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
121
+ end
122
+ sql = "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql column.type, column.limit, column.precision, column.scale}"
123
+ sql << " NOT NULL" if !allow_null.nil? && allow_null == false
124
+ do_execute sql
125
+ end
126
+
127
+ # === SQLServer Specific ======================================== #
128
+
129
+ def views
130
+ tables('VIEW')
131
+ end
132
+
133
+
134
+ protected
135
+
136
+ # === SQLServer Specific ======================================== #
137
+
138
+ def initialize_native_database_types
139
+ {
140
+ :primary_key => "int NOT NULL IDENTITY(1,1) PRIMARY KEY",
141
+ :string => { :name => native_string_database_type, :limit => 255 },
142
+ :text => { :name => native_text_database_type },
143
+ :integer => { :name => "int", :limit => 4 },
144
+ :float => { :name => "float", :limit => 8 },
145
+ :decimal => { :name => "decimal" },
146
+ :datetime => { :name => "datetime" },
147
+ :timestamp => { :name => "datetime" },
148
+ :time => { :name => native_time_database_type },
149
+ :date => { :name => native_date_database_type },
150
+ :binary => { :name => native_binary_database_type },
151
+ :boolean => { :name => "bit"},
152
+ # These are custom types that may move somewhere else for good schema_dumper.rb hacking to output them.
153
+ :char => { :name => 'char' },
154
+ :varchar_max => { :name => 'varchar(max)' },
155
+ :nchar => { :name => "nchar" },
156
+ :nvarchar => { :name => "nvarchar", :limit => 255 },
157
+ :nvarchar_max => { :name => "nvarchar(max)" },
158
+ :ntext => { :name => "ntext" },
159
+ :ss_timestamp => { :name => 'timestamp' }
160
+ }
161
+ end
162
+
163
+ def column_definitions(table_name)
164
+ db_name = Utils.unqualify_db_name(table_name)
165
+ db_name_with_period = "#{db_name}." if db_name
166
+ table_schema = Utils.unqualify_table_schema(table_name)
167
+ table_name = Utils.unqualify_table_name(table_name)
168
+ sql = %{
169
+ SELECT DISTINCT
170
+ #{lowercase_schema_reflection_sql('columns.TABLE_NAME')} AS table_name,
171
+ #{lowercase_schema_reflection_sql('columns.COLUMN_NAME')} AS name,
172
+ columns.DATA_TYPE AS type,
173
+ columns.COLUMN_DEFAULT AS default_value,
174
+ columns.NUMERIC_SCALE AS numeric_scale,
175
+ columns.NUMERIC_PRECISION AS numeric_precision,
176
+ columns.ordinal_position,
177
+ CASE
178
+ WHEN columns.DATA_TYPE IN ('nchar','nvarchar') THEN columns.CHARACTER_MAXIMUM_LENGTH
179
+ ELSE COL_LENGTH('#{db_name_with_period}'+columns.TABLE_SCHEMA+'.'+columns.TABLE_NAME, columns.COLUMN_NAME)
180
+ END AS [length],
181
+ CASE
182
+ WHEN columns.IS_NULLABLE = 'YES' THEN 1
183
+ ELSE NULL
184
+ END AS [is_nullable],
185
+ CASE
186
+ WHEN KCU.COLUMN_NAME IS NOT NULL AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY' THEN 1
187
+ ELSE NULL
188
+ END AS [is_primary],
189
+ c.is_identity AS [is_identity]
190
+ FROM #{db_name_with_period}INFORMATION_SCHEMA.COLUMNS columns
191
+ LEFT OUTER JOIN #{db_name_with_period}INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
192
+ ON TC.TABLE_NAME = columns.TABLE_NAME
193
+ AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY'
194
+ LEFT OUTER JOIN #{db_name_with_period}INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
195
+ ON KCU.COLUMN_NAME = columns.COLUMN_NAME
196
+ AND KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
197
+ AND KCU.CONSTRAINT_CATALOG = TC.CONSTRAINT_CATALOG
198
+ AND KCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA
199
+ INNER JOIN #{db_name}.sys.schemas AS s
200
+ ON s.name = columns.TABLE_SCHEMA
201
+ AND s.schema_id = s.schema_id
202
+ INNER JOIN #{db_name}.sys.objects AS o
203
+ ON s.schema_id = o.schema_id
204
+ AND o.is_ms_shipped = 0
205
+ AND o.type IN ('U', 'V')
206
+ AND o.name = columns.TABLE_NAME
207
+ INNER JOIN #{db_name}.sys.columns AS c
208
+ ON o.object_id = c.object_id
209
+ AND c.name = columns.COLUMN_NAME
210
+ WHERE columns.TABLE_NAME = @0
211
+ AND columns.TABLE_SCHEMA = #{table_schema.blank? ? "schema_name()" : "@1"}
212
+ ORDER BY columns.ordinal_position
213
+ }.gsub(/[ \t\r\n]+/,' ')
214
+ binds = [['table_name', table_name]]
215
+ binds << ['table_schema',table_schema] unless table_schema.blank?
216
+ results = do_exec_query(sql, 'SCHEMA', binds)
217
+ results.collect do |ci|
218
+ ci = ci.symbolize_keys
219
+ ci[:type] = case ci[:type]
220
+ when /^bit|image|text|ntext|datetime$/
221
+ ci[:type]
222
+ when /^numeric|decimal$/i
223
+ "#{ci[:type]}(#{ci[:numeric_precision]},#{ci[:numeric_scale]})"
224
+ when /^float|real$/i
225
+ "#{ci[:type]}(#{ci[:numeric_precision]})"
226
+ when /^char|nchar|varchar|nvarchar|varbinary|bigint|int|smallint$/
227
+ ci[:length].to_i == -1 ? "#{ci[:type]}(max)" : "#{ci[:type]}(#{ci[:length]})"
228
+ else
229
+ ci[:type]
230
+ end
231
+ if ci[:default_value].nil? && schema_cache.view_names.include?(table_name)
232
+ real_table_name = table_name_or_views_table_name(table_name)
233
+ real_column_name = views_real_column_name(table_name,ci[:name])
234
+ col_default_sql = "SELECT c.COLUMN_DEFAULT FROM #{db_name_with_period}INFORMATION_SCHEMA.COLUMNS c WHERE c.TABLE_NAME = '#{real_table_name}' AND c.COLUMN_NAME = '#{real_column_name}'"
235
+ ci[:default_value] = select_value col_default_sql, 'SCHEMA'
236
+ end
237
+ ci[:default_value] = case ci[:default_value]
238
+ when nil, '(null)', '(NULL)'
239
+ nil
240
+ when /\A\((\w+\(\))\)\Z/
241
+ ci[:default_function] = $1
242
+ nil
243
+ else
244
+ match_data = ci[:default_value].match(/\A\(+N?'?(.*?)'?\)+\Z/m)
245
+ match_data ? match_data[1] : nil
246
+ end
247
+ ci[:null] = ci[:is_nullable].to_i == 1 ; ci.delete(:is_nullable)
248
+ ci[:is_primary] = ci[:is_primary].to_i == 1
249
+ ci[:is_identity] = ci[:is_identity].to_i == 1 unless [TrueClass, FalseClass].include?(ci[:is_identity].class)
250
+ ci
251
+ end
252
+ end
253
+
254
+ def remove_check_constraints(table_name, column_name)
255
+ constraints = select_values "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{quote_string(table_name)}' and COLUMN_NAME = '#{quote_string(column_name)}'", 'SCHEMA'
256
+ constraints.each do |constraint|
257
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
258
+ end
259
+ end
260
+
261
+ def remove_default_constraint(table_name, column_name)
262
+ # If their are foreign keys in this table, we could still get back a 2D array, so flatten just in case.
263
+ execute_procedure(:sp_helpconstraint, table_name, 'nomsg').flatten.select do |row|
264
+ row['constraint_type'] == "DEFAULT on column #{column_name}"
265
+ end.each do |row|
266
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{row['constraint_name']}"
267
+ end
268
+ end
269
+
270
+ def remove_indexes(table_name, column_name)
271
+ indexes(table_name).select{ |index| index.columns.include?(column_name.to_s) }.each do |index|
272
+ remove_index(table_name, {:name => index.name})
273
+ end
274
+ end
275
+
276
+ # === SQLServer Specific (Misc Helpers) ========================= #
277
+
278
+ def get_table_name(sql)
279
+ if sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)\s+INTO\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
280
+ $2 || $3
281
+ elsif sql =~ /FROM\s+([^\(\s]+)\s*/i
282
+ $1
283
+ else
284
+ nil
285
+ end
286
+ end
287
+
288
+ def default_constraint_name(table_name, column_name)
289
+ "DF_#{table_name}_#{column_name}"
290
+ end
291
+
292
+ def detect_column_for!(table_name, column_name)
293
+ unless column = schema_cache.columns[table_name].detect { |c| c.name == column_name.to_s }
294
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
295
+ end
296
+ column
297
+ end
298
+
299
+ def lowercase_schema_reflection_sql(node)
300
+ lowercase_schema_reflection ? "LOWER(#{node})" : node
301
+ end
302
+
303
+ # === SQLServer Specific (View Reflection) ====================== #
304
+
305
+ def view_table_name(table_name)
306
+ view_info = schema_cache.view_information(table_name)
307
+ view_info ? get_table_name(view_info['VIEW_DEFINITION']) : table_name
308
+ end
309
+
310
+ def view_information(table_name)
311
+ table_name = Utils.unqualify_table_name(table_name)
312
+ view_info = select_one "SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '#{table_name}'", 'SCHEMA'
313
+ if view_info
314
+ view_info = view_info.with_indifferent_access
315
+ if view_info[:VIEW_DEFINITION].blank? || view_info[:VIEW_DEFINITION].length == 4000
316
+ view_info[:VIEW_DEFINITION] = begin
317
+ select_values("EXEC sp_helptext #{quote_table_name(table_name)}", 'SCHEMA').join
318
+ rescue
319
+ warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
320
+ nil
321
+ end
322
+ end
323
+ end
324
+ view_info
325
+ end
326
+
327
+ def table_name_or_views_table_name(table_name)
328
+ unquoted_table_name = Utils.unqualify_table_name(table_name)
329
+ schema_cache.view_names.include?(unquoted_table_name) ? view_table_name(unquoted_table_name) : unquoted_table_name
330
+ end
331
+
332
+ def views_real_column_name(table_name,column_name)
333
+ view_definition = schema_cache.view_information(table_name)[:VIEW_DEFINITION]
334
+ match_data = view_definition.match(/([\w-]*)\s+as\s+#{column_name}/im)
335
+ match_data ? match_data[1] : column_name
336
+ end
337
+
338
+ # === SQLServer Specific (Identity Inserts) ===================== #
339
+
340
+ def query_requires_identity_insert?(sql)
341
+ if insert_sql?(sql)
342
+ table_name = get_table_name(sql)
343
+ id_column = identity_column(table_name)
344
+ id_column && sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ? quote_table_name(table_name) : false
345
+ else
346
+ false
347
+ end
348
+ end
349
+
350
+ def insert_sql?(sql)
351
+ !(sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)/i).nil?
352
+ end
353
+
354
+ def with_identity_insert_enabled(table_name)
355
+ table_name = quote_table_name(table_name_or_views_table_name(table_name))
356
+ set_identity_insert(table_name, true)
357
+ yield
358
+ ensure
359
+ set_identity_insert(table_name, false)
360
+ end
361
+
362
+ def set_identity_insert(table_name, enable = true)
363
+ sql = "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
364
+ do_execute sql, 'SCHEMA'
365
+ rescue Exception => e
366
+ raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
367
+ end
368
+
369
+ def identity_column(table_name)
370
+ schema_cache.columns[table_name].detect(&:is_identity?)
371
+ end
372
+
373
+ end
374
+ end
375
+ end
376
+ end
@@ -0,0 +1,69 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sqlserver
4
+ module Showplan
5
+ class PrinterTable
6
+
7
+ cattr_accessor :max_column_width, :cell_padding
8
+ self.max_column_width = 50
9
+ self.cell_padding = 1
10
+
11
+ attr_reader :result
12
+
13
+ def initialize(result)
14
+ @result = result
15
+ end
16
+
17
+ def pp
18
+ @widths = compute_column_widths
19
+ @separator = build_separator
20
+ pp = []
21
+ pp << @separator
22
+ pp << build_cells(result.columns)
23
+ pp << @separator
24
+ result.rows.each do |row|
25
+ pp << build_cells(row)
26
+ end
27
+ pp << @separator
28
+ pp.join("\n") + "\n"
29
+ end
30
+
31
+ private
32
+
33
+ def compute_column_widths
34
+ [].tap do |computed_widths|
35
+ result.columns.each_with_index do |column, i|
36
+ cells_in_column = [column] + result.rows.map { |r| cast_item(r[i]) }
37
+ computed_width = cells_in_column.map(&:length).max
38
+ final_width = computed_width > max_column_width ? max_column_width : computed_width
39
+ computed_widths << final_width
40
+ end
41
+ end
42
+ end
43
+
44
+ def build_separator
45
+ '+' + @widths.map {|w| '-' * (w + (cell_padding*2))}.join('+') + '+'
46
+ end
47
+
48
+ def build_cells(items)
49
+ cells = []
50
+ items.each_with_index do |item, i|
51
+ cells << cast_item(item).ljust(@widths[i])
52
+ end
53
+ "| #{cells.join(' | ')} |"
54
+ end
55
+
56
+ def cast_item(item)
57
+ case item
58
+ when NilClass then 'NULL'
59
+ when Float then item.to_s.to(9)
60
+ else item.to_s.truncate(max_column_width)
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,25 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sqlserver
4
+ module Showplan
5
+ class PrinterXml
6
+
7
+ def initialize(result)
8
+ @result = result
9
+ end
10
+
11
+ def pp
12
+ xml = @result.rows.first.first
13
+ if defined?(Nokogiri)
14
+ Nokogiri::XML(xml).to_xml :indent => 2, :encoding => 'UTF-8'
15
+ else
16
+ xml
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,67 @@
1
+ require 'active_record/connection_adapters/sqlserver/showplan/printer_table'
2
+ require 'active_record/connection_adapters/sqlserver/showplan/printer_xml'
3
+
4
+ module ActiveRecord
5
+ module ConnectionAdapters
6
+ module Sqlserver
7
+ module Showplan
8
+
9
+ OPTION_ALL = 'SHOWPLAN_ALL'
10
+ OPTION_TEXT = 'SHOWPLAN_TEXT'
11
+ OPTION_XML = 'SHOWPLAN_XML'
12
+ OPTIONS = [OPTION_ALL, OPTION_TEXT, OPTION_XML]
13
+
14
+ def explain(arel, binds = [])
15
+ sql = to_sql(arel)
16
+ result = with_showplan_on { do_exec_query(sql, 'EXPLAIN', binds) }
17
+ printer = showplan_printer.new(result)
18
+ printer.pp
19
+ end
20
+
21
+
22
+ protected
23
+
24
+ def with_showplan_on
25
+ set_showplan_option(true)
26
+ yield
27
+ ensure
28
+ set_showplan_option(false)
29
+ end
30
+
31
+ def set_showplan_option(enable = true)
32
+ sql = "SET #{option} #{enable ? 'ON' : 'OFF'}"
33
+ raw_connection_do(sql)
34
+ rescue Exception => e
35
+ raise ActiveRecordError, "#{option} could not be turned #{enable ? 'ON' : 'OFF'}, perhaps you do not have SHOWPLAN permissions?"
36
+ end
37
+
38
+ def option
39
+ (SQLServerAdapter.showplan_option || OPTION_ALL).tap do |opt|
40
+ raise(ArgumentError, "Unknown SHOWPLAN option #{opt.inspect} found.") if OPTIONS.exclude?(opt)
41
+ end
42
+ end
43
+
44
+ def showplan_all?
45
+ option == OPTION_ALL
46
+ end
47
+
48
+ def showplan_text?
49
+ option == OPTION_TEXT
50
+ end
51
+
52
+ def showplan_xml?
53
+ option == OPTION_XML
54
+ end
55
+
56
+ def showplan_printer
57
+ case option
58
+ when OPTION_XML then PrinterXml
59
+ when OPTION_ALL, OPTION_TEXT then PrinterTable
60
+ else PrinterTable
61
+ end
62
+ end
63
+
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,32 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sqlserver
4
+ class Utils
5
+
6
+ class << self
7
+
8
+ def unquote_string(string)
9
+ string.to_s.gsub(/\'\'/, "'")
10
+ end
11
+
12
+ def unqualify_table_name(table_name)
13
+ table_name.to_s.split('.').last.tr('[]','')
14
+ end
15
+
16
+ def unqualify_table_schema(table_name)
17
+ table_name.to_s.split('.')[-2].gsub(/[\[\]]/,'') rescue nil
18
+ end
19
+
20
+ def unqualify_db_name(table_name)
21
+ table_names = table_name.to_s.split('.')
22
+ table_names.length == 3 ? table_names.first.tr('[]','') : nil
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+