activerecord 3.1.0.beta1 → 3.1.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG +123 -30
- data/README.rdoc +2 -2
- data/lib/active_record/associations.rb +10 -9
- data/lib/active_record/associations/alias_tracker.rb +2 -2
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +1 -1
- data/lib/active_record/associations/builder/singular_association.rb +19 -6
- data/lib/active_record/associations/collection_association.rb +61 -52
- data/lib/active_record/associations/collection_proxy.rb +30 -3
- data/lib/active_record/associations/has_one_association.rb +1 -1
- data/lib/active_record/associations/join_dependency/join_association.rb +3 -3
- data/lib/active_record/associations/join_helper.rb +1 -1
- data/lib/active_record/associations/singular_association.rb +13 -12
- data/lib/active_record/associations/through_association.rb +4 -1
- data/lib/active_record/base.rb +95 -46
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +1 -9
- data/lib/active_record/connection_adapters/column.rb +1 -1
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +8 -3
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +28 -26
- data/lib/active_record/fixtures.rb +294 -339
- data/lib/active_record/identity_map.rb +34 -8
- data/lib/active_record/locking/optimistic.rb +16 -16
- data/lib/active_record/locking/pessimistic.rb +2 -2
- data/lib/active_record/observer.rb +3 -3
- data/lib/active_record/persistence.rb +1 -1
- data/lib/active_record/railties/controller_runtime.rb +10 -1
- data/lib/active_record/railties/databases.rake +9 -9
- data/lib/active_record/relation/calculations.rb +5 -6
- data/lib/active_record/relation/finder_methods.rb +9 -4
- data/lib/active_record/serialization.rb +1 -1
- data/lib/active_record/session_store.rb +4 -2
- data/lib/active_record/test_case.rb +7 -0
- data/lib/active_record/validations.rb +3 -3
- data/lib/active_record/version.rb +1 -1
- data/lib/rails/generators/active_record/model/templates/migration.rb +1 -1
- metadata +6 -6
@@ -64,9 +64,12 @@ module ActiveRecord
|
|
64
64
|
|
65
65
|
def method_missing(method, *args, &block)
|
66
66
|
match = DynamicFinderMatch.match(method)
|
67
|
-
if match && match.
|
68
|
-
|
69
|
-
|
67
|
+
if match && match.instantiator?
|
68
|
+
record = send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
|
69
|
+
@association.send :set_owner_attributes, r
|
70
|
+
@association.send :add_to_target, r
|
71
|
+
yield(r) if block_given?
|
72
|
+
end
|
70
73
|
end
|
71
74
|
|
72
75
|
if target.respond_to?(method) || (!@association.klass.respond_to?(method) && Class.respond_to?(method))
|
@@ -120,6 +123,30 @@ module ActiveRecord
|
|
120
123
|
method_missing(:new, *args, &block)
|
121
124
|
end
|
122
125
|
end
|
126
|
+
|
127
|
+
def proxy_owner
|
128
|
+
ActiveSupport::Deprecation.warn(
|
129
|
+
"Calling record.#{@association.reflection.name}.proxy_owner is deprecated. Please use " \
|
130
|
+
"record.association(:#{@association.reflection.name}).owner instead."
|
131
|
+
)
|
132
|
+
@association.owner
|
133
|
+
end
|
134
|
+
|
135
|
+
def proxy_target
|
136
|
+
ActiveSupport::Deprecation.warn(
|
137
|
+
"Calling record.#{@association.reflection.name}.proxy_target is deprecated. Please use " \
|
138
|
+
"record.association(:#{@association.reflection.name}).target instead."
|
139
|
+
)
|
140
|
+
@association.target
|
141
|
+
end
|
142
|
+
|
143
|
+
def proxy_reflection
|
144
|
+
ActiveSupport::Deprecation.warn(
|
145
|
+
"Calling record.#{@association.reflection.name}.proxy_reflection is deprecated. Please use " \
|
146
|
+
"record.association(:#{@association.reflection.name}).reflection instead."
|
147
|
+
)
|
148
|
+
@association.reflection
|
149
|
+
end
|
123
150
|
end
|
124
151
|
end
|
125
152
|
end
|
@@ -91,12 +91,12 @@ module ActiveRecord
|
|
91
91
|
|
92
92
|
constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
|
93
93
|
|
94
|
-
relation.from(join(table, constraint))
|
95
|
-
|
96
94
|
unless conditions[i].empty?
|
97
|
-
|
95
|
+
constraint = constraint.and(sanitize(conditions[i], table))
|
98
96
|
end
|
99
97
|
|
98
|
+
relation.from(join(table, constraint))
|
99
|
+
|
100
100
|
# The current table in this iteration becomes the foreign table in the next
|
101
101
|
foreign_table = table
|
102
102
|
end
|
@@ -32,7 +32,7 @@ module ActiveRecord
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def table_alias_for(reflection, join = false)
|
35
|
-
name = alias_tracker.pluralize(reflection.name)
|
35
|
+
name = alias_tracker.pluralize(reflection.name, reflection.active_record)
|
36
36
|
name << "_#{alias_suffix}"
|
37
37
|
name << "_join" if join
|
38
38
|
name
|
@@ -17,20 +17,28 @@ module ActiveRecord
|
|
17
17
|
replace(record)
|
18
18
|
end
|
19
19
|
|
20
|
-
def create(attributes = {}, options = {})
|
21
|
-
|
20
|
+
def create(attributes = {}, options = {}, &block)
|
21
|
+
build(attributes, options, &block).tap { |record| record.save }
|
22
22
|
end
|
23
23
|
|
24
|
-
def create!(attributes = {}, options = {})
|
25
|
-
build(attributes, options).tap { |record| record.save! }
|
24
|
+
def create!(attributes = {}, options = {}, &block)
|
25
|
+
build(attributes, options, &block).tap { |record| record.save! }
|
26
26
|
end
|
27
27
|
|
28
28
|
def build(attributes = {}, options = {})
|
29
|
-
|
29
|
+
record = reflection.build_association(attributes, options)
|
30
|
+
record.assign_attributes(create_scope.except(*record.changed), :without_protection => true)
|
31
|
+
yield(record) if block_given?
|
32
|
+
set_new_record(record)
|
33
|
+
record
|
30
34
|
end
|
31
35
|
|
32
36
|
private
|
33
37
|
|
38
|
+
def create_scope
|
39
|
+
scoped.scope_for_create.stringify_keys.except(klass.primary_key)
|
40
|
+
end
|
41
|
+
|
34
42
|
def find_target
|
35
43
|
scoped.first.tap { |record| set_inverse_instance(record) }
|
36
44
|
end
|
@@ -43,13 +51,6 @@ module ActiveRecord
|
|
43
51
|
def set_new_record(record)
|
44
52
|
replace(record)
|
45
53
|
end
|
46
|
-
|
47
|
-
def new_record(method, attributes, options)
|
48
|
-
attributes = scoped.scope_for_create.merge(attributes || {})
|
49
|
-
record = reflection.send("#{method}_association", attributes, options)
|
50
|
-
set_new_record(record)
|
51
|
-
record
|
52
|
-
end
|
53
54
|
end
|
54
55
|
end
|
55
56
|
end
|
@@ -14,7 +14,10 @@ module ActiveRecord
|
|
14
14
|
def target_scope
|
15
15
|
scope = super
|
16
16
|
chain[1..-1].each do |reflection|
|
17
|
-
scope = scope.merge(
|
17
|
+
scope = scope.merge(
|
18
|
+
reflection.klass.scoped.with_default_scope.
|
19
|
+
except(:select, :create_with)
|
20
|
+
)
|
18
21
|
end
|
19
22
|
scope
|
20
23
|
end
|
data/lib/active_record/base.rb
CHANGED
@@ -393,8 +393,8 @@ module ActiveRecord #:nodoc:
|
|
393
393
|
# Indicates whether table names should be the pluralized versions of the corresponding class names.
|
394
394
|
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
|
395
395
|
# See table_name for the full rules on table/class naming. This is true, by default.
|
396
|
-
|
397
|
-
|
396
|
+
class_attribute :pluralize_table_names, :instance_writer => false
|
397
|
+
self.pluralize_table_names = true
|
398
398
|
|
399
399
|
##
|
400
400
|
# :singleton-method:
|
@@ -482,7 +482,7 @@ module ActiveRecord #:nodoc:
|
|
482
482
|
# # Create a single new object
|
483
483
|
# User.create(:first_name => 'Jamie')
|
484
484
|
#
|
485
|
-
# # Create a single new object using the :admin mass-assignment security
|
485
|
+
# # Create a single new object using the :admin mass-assignment security role
|
486
486
|
# User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
|
487
487
|
#
|
488
488
|
# # Create a single new object bypassing mass-assignment security
|
@@ -577,15 +577,25 @@ module ActiveRecord #:nodoc:
|
|
577
577
|
#
|
578
578
|
# ==== Examples
|
579
579
|
#
|
580
|
-
# class Invoice < ActiveRecord::Base
|
580
|
+
# class Invoice < ActiveRecord::Base
|
581
|
+
# end
|
582
|
+
#
|
581
583
|
# file class table_name
|
582
584
|
# invoice.rb Invoice invoices
|
583
585
|
#
|
584
|
-
# class Invoice < ActiveRecord::Base
|
586
|
+
# class Invoice < ActiveRecord::Base
|
587
|
+
# class Lineitem < ActiveRecord::Base
|
588
|
+
# end
|
589
|
+
# end
|
590
|
+
#
|
585
591
|
# file class table_name
|
586
592
|
# invoice.rb Invoice::Lineitem invoice_lineitems
|
587
593
|
#
|
588
|
-
# module Invoice
|
594
|
+
# module Invoice
|
595
|
+
# class Lineitem < ActiveRecord::Base
|
596
|
+
# end
|
597
|
+
# end
|
598
|
+
#
|
589
599
|
# file class table_name
|
590
600
|
# invoice/lineitem.rb Invoice::Lineitem lineitems
|
591
601
|
#
|
@@ -767,6 +777,17 @@ module ActiveRecord #:nodoc:
|
|
767
777
|
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
|
768
778
|
end
|
769
779
|
|
780
|
+
# Returns an array of column names as strings if it's not
|
781
|
+
# an abstract class and table exists.
|
782
|
+
# Otherwise it returns an empty array.
|
783
|
+
def attribute_names
|
784
|
+
@attribute_names ||= if !abstract_class? && table_exists?
|
785
|
+
column_names
|
786
|
+
else
|
787
|
+
[]
|
788
|
+
end
|
789
|
+
end
|
790
|
+
|
770
791
|
# Set the lookup ancestors for ActiveModel.
|
771
792
|
def lookup_ancestors #:nodoc:
|
772
793
|
klass = self
|
@@ -830,6 +851,10 @@ module ActiveRecord #:nodoc:
|
|
830
851
|
@symbolized_base_class ||= base_class.to_s.to_sym
|
831
852
|
end
|
832
853
|
|
854
|
+
def symbolized_sti_name
|
855
|
+
@symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
|
856
|
+
end
|
857
|
+
|
833
858
|
# Returns the base AR subclass that this class descends from. If A
|
834
859
|
# extends AR::Base, A.base_class will return A. If B descends from A
|
835
860
|
# through some arbitrarily deep hierarchy, B.base_class will return A.
|
@@ -891,7 +916,7 @@ module ActiveRecord #:nodoc:
|
|
891
916
|
# not use the default_scope:
|
892
917
|
#
|
893
918
|
# Post.unscoped {
|
894
|
-
# limit(10) # Fires "SELECT * FROM posts LIMIT 10"
|
919
|
+
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
|
895
920
|
# }
|
896
921
|
#
|
897
922
|
# It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt>
|
@@ -1482,7 +1507,7 @@ MSG
|
|
1482
1507
|
# # Instantiates a single new object
|
1483
1508
|
# User.new(:first_name => 'Jamie')
|
1484
1509
|
#
|
1485
|
-
# # Instantiates a single new object using the :admin mass-assignment security
|
1510
|
+
# # Instantiates a single new object using the :admin mass-assignment security role
|
1486
1511
|
# User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
|
1487
1512
|
#
|
1488
1513
|
# # Instantiates a single new object bypassing mass-assignment security
|
@@ -1648,17 +1673,16 @@ MSG
|
|
1648
1673
|
|
1649
1674
|
return unless new_attributes.is_a?(Hash)
|
1650
1675
|
|
1651
|
-
guard_protected_attributes
|
1652
|
-
if guard_protected_attributes
|
1653
|
-
assign_attributes(new_attributes)
|
1654
|
-
else
|
1676
|
+
if guard_protected_attributes == false
|
1655
1677
|
assign_attributes(new_attributes, :without_protection => true)
|
1678
|
+
else
|
1679
|
+
assign_attributes(new_attributes)
|
1656
1680
|
end
|
1657
1681
|
end
|
1658
1682
|
|
1659
1683
|
# Allows you to set all the attributes for a particular mass-assignment
|
1660
|
-
# security
|
1661
|
-
# the attribute names (which again matches the column names) and the
|
1684
|
+
# security role by passing in a hash of attributes with keys matching
|
1685
|
+
# the attribute names (which again matches the column names) and the role
|
1662
1686
|
# name using the :as option.
|
1663
1687
|
#
|
1664
1688
|
# To bypass mass-assignment security you can use the :without_protection => true
|
@@ -1684,13 +1708,15 @@ MSG
|
|
1684
1708
|
# user.name # => "Josh"
|
1685
1709
|
# user.is_admin? # => true
|
1686
1710
|
def assign_attributes(new_attributes, options = {})
|
1711
|
+
return unless new_attributes
|
1712
|
+
|
1687
1713
|
attributes = new_attributes.stringify_keys
|
1688
|
-
|
1714
|
+
role = options[:as] || :default
|
1689
1715
|
|
1690
1716
|
multi_parameter_attributes = []
|
1691
1717
|
|
1692
1718
|
unless options[:without_protection]
|
1693
|
-
attributes = sanitize_for_mass_assignment(attributes,
|
1719
|
+
attributes = sanitize_for_mass_assignment(attributes, role)
|
1694
1720
|
end
|
1695
1721
|
|
1696
1722
|
attributes.each do |k, v|
|
@@ -1943,32 +1969,9 @@ MSG
|
|
1943
1969
|
errors = []
|
1944
1970
|
callstack.each do |name, values_with_empty_parameters|
|
1945
1971
|
begin
|
1946
|
-
|
1947
|
-
# in order to allow a date to be set without a year, we must keep the empty values.
|
1948
|
-
# Otherwise, we wouldn't be able to distinguish it from a date with an empty day.
|
1949
|
-
values = values_with_empty_parameters.reject { |v| v.nil? }
|
1950
|
-
|
1951
|
-
if values.empty?
|
1952
|
-
send(name + "=", nil)
|
1953
|
-
else
|
1954
|
-
|
1955
|
-
value = if Time == klass
|
1956
|
-
instantiate_time_object(name, values)
|
1957
|
-
elsif Date == klass
|
1958
|
-
begin
|
1959
|
-
values = values_with_empty_parameters.collect do |v| v.nil? ? 1 : v end
|
1960
|
-
Date.new(*values)
|
1961
|
-
rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
|
1962
|
-
instantiate_time_object(name, values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
|
1963
|
-
end
|
1964
|
-
else
|
1965
|
-
klass.new(*values)
|
1966
|
-
end
|
1967
|
-
|
1968
|
-
send(name + "=", value)
|
1969
|
-
end
|
1972
|
+
send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
|
1970
1973
|
rescue => ex
|
1971
|
-
errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
|
1974
|
+
errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
|
1972
1975
|
end
|
1973
1976
|
end
|
1974
1977
|
unless errors.empty?
|
@@ -1976,19 +1979,65 @@ MSG
|
|
1976
1979
|
end
|
1977
1980
|
end
|
1978
1981
|
|
1982
|
+
def read_value_from_parameter(name, values_hash_from_param)
|
1983
|
+
klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
|
1984
|
+
if values_hash_from_param.values.all?{|v|v.nil?}
|
1985
|
+
nil
|
1986
|
+
elsif klass == Time
|
1987
|
+
read_time_parameter_value(name, values_hash_from_param)
|
1988
|
+
elsif klass == Date
|
1989
|
+
read_date_parameter_value(name, values_hash_from_param)
|
1990
|
+
else
|
1991
|
+
read_other_parameter_value(klass, name, values_hash_from_param)
|
1992
|
+
end
|
1993
|
+
end
|
1994
|
+
|
1995
|
+
def read_time_parameter_value(name, values_hash_from_param)
|
1996
|
+
# If Date bits were not provided, error
|
1997
|
+
raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
|
1998
|
+
max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
|
1999
|
+
set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
|
2000
|
+
# If Date bits were provided but blank, then default to 1
|
2001
|
+
# If Time bits are not there, then default to 0
|
2002
|
+
[1,1,1,0,0,0].each_with_index{|v,i| set_values[i] = set_values[i].blank? ? v : set_values[i]}
|
2003
|
+
instantiate_time_object(name, set_values)
|
2004
|
+
end
|
2005
|
+
|
2006
|
+
def read_date_parameter_value(name, values_hash_from_param)
|
2007
|
+
set_values = (1..3).collect{|position| values_hash_from_param[position].blank? ? 1 : values_hash_from_param[position]}
|
2008
|
+
begin
|
2009
|
+
Date.new(*set_values)
|
2010
|
+
rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
|
2011
|
+
instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
|
2012
|
+
end
|
2013
|
+
end
|
2014
|
+
|
2015
|
+
def read_other_parameter_value(klass, name, values_hash_from_param)
|
2016
|
+
max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
|
2017
|
+
values = (1..max_position).collect do |position|
|
2018
|
+
raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
|
2019
|
+
values_hash_from_param[position]
|
2020
|
+
end
|
2021
|
+
klass.new(*values)
|
2022
|
+
end
|
2023
|
+
|
2024
|
+
def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
|
2025
|
+
[values_hash_from_param.keys.max,upper_cap].min
|
2026
|
+
end
|
2027
|
+
|
1979
2028
|
def extract_callstack_for_multiparameter_attributes(pairs)
|
1980
2029
|
attributes = { }
|
1981
2030
|
|
1982
|
-
|
2031
|
+
pairs.each do |pair|
|
1983
2032
|
multiparameter_name, value = pair
|
1984
2033
|
attribute_name = multiparameter_name.split("(").first
|
1985
|
-
attributes[attribute_name] =
|
2034
|
+
attributes[attribute_name] = {} unless attributes.include?(attribute_name)
|
1986
2035
|
|
1987
2036
|
parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
|
1988
|
-
attributes[attribute_name]
|
2037
|
+
attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
|
1989
2038
|
end
|
1990
2039
|
|
1991
|
-
attributes
|
2040
|
+
attributes
|
1992
2041
|
end
|
1993
2042
|
|
1994
2043
|
def type_cast_attribute_value(multiparameter_name, value)
|
@@ -1996,7 +2045,7 @@ MSG
|
|
1996
2045
|
end
|
1997
2046
|
|
1998
2047
|
def find_parameter_position(multiparameter_name)
|
1999
|
-
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first
|
2048
|
+
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
|
2000
2049
|
end
|
2001
2050
|
|
2002
2051
|
# Returns a comma-separated pair list, like "key1 = val1, key2 = val2".
|
@@ -6,15 +6,7 @@ module ActiveRecord
|
|
6
6
|
# Returns an array of record hashes with the column names as keys and
|
7
7
|
# column values as values.
|
8
8
|
def select_all(sql, name = nil, binds = [])
|
9
|
-
|
10
|
-
select(sql, name, binds)
|
11
|
-
else
|
12
|
-
return select(sql, name) if binds.empty?
|
13
|
-
binds = binds.dup
|
14
|
-
select sql.gsub('?') {
|
15
|
-
quote(*binds.shift.reverse)
|
16
|
-
}, name
|
17
|
-
end
|
9
|
+
select(sql, name, binds)
|
18
10
|
end
|
19
11
|
|
20
12
|
# Returns a record hash with the column names as keys and column values
|
@@ -189,7 +189,7 @@ module ActiveRecord
|
|
189
189
|
|
190
190
|
def new_time(year, mon, mday, hour, min, sec, microsec)
|
191
191
|
# Treat 0000-00-00 00:00:00 as nil.
|
192
|
-
return nil if year.nil? || year == 0
|
192
|
+
return nil if year.nil? || (year == 0 && mon == 0 && mday == 0)
|
193
193
|
|
194
194
|
Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
|
195
195
|
end
|
@@ -184,6 +184,10 @@ module ActiveRecord
|
|
184
184
|
QUOTED_FALSE
|
185
185
|
end
|
186
186
|
|
187
|
+
def substitute_at(column, index)
|
188
|
+
Arel.sql "\0"
|
189
|
+
end
|
190
|
+
|
187
191
|
# REFERENTIAL INTEGRITY ====================================
|
188
192
|
|
189
193
|
def disable_referential_integrity(&block) #:nodoc:
|
@@ -292,14 +296,14 @@ module ActiveRecord
|
|
292
296
|
binds = binds.dup
|
293
297
|
|
294
298
|
# Pretend to support bind parameters
|
295
|
-
execute sql.gsub(
|
299
|
+
execute sql.gsub("\0") { quote(*binds.shift.reverse) }, name
|
296
300
|
end
|
297
301
|
|
298
302
|
def exec_delete(sql, name, binds)
|
299
303
|
binds = binds.dup
|
300
304
|
|
301
305
|
# Pretend to support bind parameters
|
302
|
-
execute sql.gsub(
|
306
|
+
execute sql.gsub("\0") { quote(*binds.shift.reverse) }, name
|
303
307
|
@connection.affected_rows
|
304
308
|
end
|
305
309
|
alias :exec_update :exec_delete
|
@@ -646,7 +650,8 @@ module ActiveRecord
|
|
646
650
|
# Returns an array of record hashes with the column names as keys and
|
647
651
|
# column values as values.
|
648
652
|
def select(sql, name = nil, binds = [])
|
649
|
-
|
653
|
+
binds = binds.dup
|
654
|
+
exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a
|
650
655
|
end
|
651
656
|
|
652
657
|
def exec_query(sql, name = 'SQL', binds = [])
|