Skip to content

Commit ce570d6

Browse files
committed
Better bind types & params for sp_executesql. Fixes #239.
Fixed: RuntimeError: Unknown bind columns. We can account for this. * HasManyAssociationsTest#test_with_polymorphic_has_many_with_custom_columns_name: * HasOneAssociationsTest#test_with_polymorphic_has_one_with_custom_columns_name: 12 failures, 6 errors, 2 skips
1 parent 8aff88b commit ce570d6

File tree

5 files changed

+48
-36
lines changed

5 files changed

+48
-36
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
#### Fixed
5050

5151
* Default lock is now "WITH(UPDLOCK)". Fixes #368
52+
* Better bind types & params for `sp_executesql`. Fixes #239.
5253

5354
#### Security
5455

lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def exec_explain(queries)
1616

1717
# This is somewhat hacky, but it should reliably reformat our prepared sql statment
1818
# which uses sp_executesql to just the first argument, then unquote it. Likewise our
19-
# do_exec_query method should substitude the @n args withe the quoted values.
19+
# `sp_executesql` method should substitude the @n args withe the quoted values.
2020
def unprepare_sqlserver_statement(sql)
2121
if sql.starts_with?(SQLSERVER_STATEMENT_PREFIX)
2222
executesql = sql.from(SQLSERVER_STATEMENT_PREFIX.length)

lib/active_record/connection_adapters/sqlserver/database_statements.rb

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module SQLServer
44
module DatabaseStatements
55

66
def select_rows(sql, name = nil, binds = [])
7-
do_exec_query sql, name, binds, fetch: :rows
7+
sp_executesql sql, name, binds, fetch: :rows
88
end
99

1010
def execute(sql, name = nil)
@@ -18,9 +18,9 @@ def execute(sql, name = nil)
1818
def exec_query(sql, name = 'SQL', binds = [], sqlserver_options = {})
1919
if update_sql?(sql)
2020
sql = strip_ident_from_update(sql)
21-
do_exec_query(sql, name, binds)
21+
sp_executesql(sql, name, binds)
2222
else
23-
do_exec_query(sql, name, binds)
23+
sp_executesql(sql, name, binds)
2424
end
2525
end
2626

@@ -307,42 +307,45 @@ def do_execute(sql, name = 'SQL')
307307
log(sql, name) { raw_connection_do(sql) }
308308
end
309309

310-
def do_exec_query(sql, name, binds, options = {})
311-
# This allows non-AR code to utilize the binds
312-
# handling code, e.g. select_rows()
313-
if options[:fetch] != :rows
314-
options[:ar_result] = true
315-
end
316-
explaining = name == 'EXPLAIN'
317-
names_and_types = []
318-
params = []
310+
def sp_executesql(sql, name, binds, options = {})
311+
options[:ar_result] = true if options[:fetch] != :rows
312+
types, params = sp_executesql_types_and_parameters(binds)
313+
sql = sp_executesql_sql(sql, types, params, name)
314+
raw_select sql, name, binds, options
315+
end
316+
317+
def sp_executesql_types_and_parameters(binds)
318+
types, params = [], []
319319
binds.each_with_index do |(column, value), index|
320-
ar_column = column.is_a?(ActiveRecord::ConnectionAdapters::Column)
321-
next if ar_column && column.sql_type == 'timestamp'
322-
v = value
323-
names_and_types << if ar_column
324-
"@#{index} #{column.sql_type_for_statement}"
325-
elsif column.acts_like?(:string)
326-
"@#{index} nvarchar(max)"
327-
elsif column.is_a?(Fixnum)
328-
v = value.to_i
329-
"@#{index} int"
330-
else
331-
raise 'Unknown bind columns. We can account for this.'
332-
end
333-
quoted_value = ar_column ? quote(v, column) : quote(v, nil)
334-
params << (explaining ? quoted_value : "@#{index} = #{quoted_value}")
320+
types << "@#{index} #{sp_executesql_sql_type(column, value)}"
321+
params << quote(value, column)
335322
end
336-
if explaining
337-
params.each_with_index do |param, index|
323+
[types, params]
324+
end
325+
326+
def sp_executesql_sql_type(column, value)
327+
return column.sql_type_for_statement if SQLServerColumn === column
328+
if value.is_a?(Numeric)
329+
'int'
330+
# We can do more here later.
331+
else
332+
'nvarchar(max)'
333+
end
334+
end
335+
336+
def sp_executesql_sql(sql, types, params, name)
337+
if name == 'EXPLAIN'
338+
params.each.with_index do |param, index|
338339
substitute_at_finder = /(@#{index})(?=(?:[^']|'[^']*')*$)/ # Finds unquoted @n values.
339340
sql.sub! substitute_at_finder, param
340341
end
341342
else
343+
types = quote(types.join(', '))
344+
params = params.map.with_index{ |p, i| "@#{i} = #{p}" }.join(', ') # Only p is needed, but with @i helps explain regexp.
342345
sql = "EXEC sp_executesql #{quote(sql)}"
343-
sql << ", #{quote(names_and_types.join(', '))}, #{params.join(', ')}" unless binds.empty?
346+
sql << ", #{types}, #{params}" unless params.empty?
344347
end
345-
raw_select sql, name, binds, options
348+
sql
346349
end
347350

348351
def raw_connection_do(sql)

lib/active_record/connection_adapters/sqlserver/schema_statements.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,9 @@ def column_definitions(table_name)
244244
AND columns.TABLE_SCHEMA = #{identifier.schema.blank? ? 'schema_name()' : '@1'}
245245
ORDER BY columns.ordinal_position
246246
}.gsub(/[ \t\r\n]+/, ' ')
247-
binds = [['table_name', identifier.object]]
248-
binds << ['table_schema', identifier.schema] unless identifier.schema.blank?
249-
results = do_exec_query(sql, 'SCHEMA', binds)
247+
binds = [[info_schema_table_name_column, identifier.object]]
248+
binds << [info_schema_table_schema_column, identifier.schema] unless identifier.schema.blank?
249+
results = sp_executesql(sql, 'SCHEMA', binds)
250250
results.map do |ci|
251251
ci = ci.symbolize_keys
252252
ci[:_type] = ci[:type]
@@ -302,6 +302,14 @@ def column_definitions(table_name)
302302
end
303303
end
304304

305+
def info_schema_table_name_column
306+
@info_schema_table_name_column ||= new_column 'table_name', nil, lookup_cast_type('nvarchar(128)'), 'nvarchar(128)', true
307+
end
308+
309+
def info_schema_table_schema_column
310+
@info_schema_table_schema_column ||= new_column 'table_schema', nil, lookup_cast_type('nvarchar(128)'), 'nvarchar(128)', true
311+
end
312+
305313
def remove_check_constraints(table_name, column_name)
306314
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'
307315
constraints.each do |constraint|

lib/active_record/connection_adapters/sqlserver/showplan.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ module Showplan
1313

1414
def explain(arel, binds = [])
1515
sql = to_sql(arel)
16-
result = with_showplan_on { do_exec_query(sql, 'EXPLAIN', binds) }
16+
result = with_showplan_on { sp_executesql(sql, 'EXPLAIN', binds) }
1717
printer = showplan_printer.new(result)
1818
printer.pp
1919
end

0 commit comments

Comments
 (0)