activerecord-simpledb-adapter 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -0
- data/Gemfile.lock +40 -38
- data/activerecord-simpledb-adapter.gemspec +23 -11
- data/lib/active_record/connection_adapters/simpledb_adapter.rb +8 -466
- data/lib/active_record/connection_adapters/simpledb_adapter/adapter.rb +258 -0
- data/lib/active_record/connection_adapters/simpledb_adapter/base.rb +65 -0
- data/lib/active_record/connection_adapters/simpledb_adapter/column.rb +62 -0
- data/lib/active_record/connection_adapters/simpledb_adapter/finder_methods.rb +20 -0
- data/lib/active_record/connection_adapters/simpledb_adapter/misc/aws_overrides.rb +53 -0
- data/lib/active_record/connection_adapters/simpledb_adapter/misc/simpledb_logger.rb +15 -0
- data/lib/active_record/connection_adapters/simpledb_adapter/table_definition.rb +32 -0
- data/lib/active_record/connection_adapters/simpledb_adapter/validations.rb +50 -0
- data/spec/active_record/batch_spec.rb +67 -0
- data/spec/spec_helper.rb +8 -1
- metadata +67 -43
@@ -0,0 +1,258 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class SimpleDBAdapter < AbstractAdapter
|
4
|
+
@@collections = {}
|
5
|
+
@@ccn = {}
|
6
|
+
|
7
|
+
def self.set_collection_columns table_name, columns_definition
|
8
|
+
@@collections[table_name] = columns_definition
|
9
|
+
@@ccn[table_name] = columns_definition.collection_column_name
|
10
|
+
end
|
11
|
+
|
12
|
+
def columns_definition table_name
|
13
|
+
@@collections[table_name]
|
14
|
+
end
|
15
|
+
|
16
|
+
def collection_column_name table_name
|
17
|
+
@@ccn[table_name]
|
18
|
+
end
|
19
|
+
|
20
|
+
ADAPTER_NAME = 'SimpleDB'.freeze
|
21
|
+
|
22
|
+
def adapter_name
|
23
|
+
ADAPTER_NAME
|
24
|
+
end
|
25
|
+
|
26
|
+
NIL_REPRESENTATION = "Aws::Nil".freeze
|
27
|
+
|
28
|
+
def nil_representation
|
29
|
+
NIL_REPRESENTATION
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def supports_count_distinct?; false; end
|
34
|
+
|
35
|
+
#========= QUOTING =====================
|
36
|
+
|
37
|
+
#dirty hack for removing all (') from value for hash table attrubutes
|
38
|
+
def hash_value_quote(value, column = nil)
|
39
|
+
return nil if value.nil?
|
40
|
+
quote(value, column).gsub /^'*|'*$/, ''
|
41
|
+
end
|
42
|
+
|
43
|
+
def quote(value, column = nil)
|
44
|
+
if value.present? && column.present? && column.number?
|
45
|
+
"'#{column.quote_number value}'"
|
46
|
+
elsif value.nil?
|
47
|
+
"'#{nil_representation}'"
|
48
|
+
else
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def quote_column_name(column_name)
|
54
|
+
"`#{column_name}`"
|
55
|
+
end
|
56
|
+
|
57
|
+
def quote_table_name(table_name)
|
58
|
+
table_name
|
59
|
+
end
|
60
|
+
#=======================================
|
61
|
+
#======== BATCHES ==========
|
62
|
+
def begin_batch type
|
63
|
+
raise ActiveRecord::ActiveRecordError.new("Batch already started. Finish it before start new batch") \
|
64
|
+
if defined?(@batch_type) && !@batch_type.nil?
|
65
|
+
|
66
|
+
@batch_type = type
|
67
|
+
end
|
68
|
+
|
69
|
+
def commit_batch
|
70
|
+
log({:type => @batch_type, :count => batch_pool.count }.inspect, "SimpleDB Batch Operation") do
|
71
|
+
case @batch_type
|
72
|
+
when :update
|
73
|
+
@connection.batch_put_attributes domain_name, batch_pool
|
74
|
+
when :delete
|
75
|
+
@connection.batch_delete_attributes domain_name, batch_pool
|
76
|
+
end
|
77
|
+
clear_batch
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def clear_batch
|
82
|
+
batch_pool.clear
|
83
|
+
@batch_type = nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def is_batch type
|
87
|
+
type = :update if type == :insert
|
88
|
+
defined?(@batch_type) && @batch_type == type
|
89
|
+
end
|
90
|
+
|
91
|
+
#===========================
|
92
|
+
attr_reader :domain_name
|
93
|
+
|
94
|
+
def initialize(connection, logger, aws_key, aws_secret, domain_name, connection_parameters, config)
|
95
|
+
super(connection, logger)
|
96
|
+
@config = config
|
97
|
+
@domain_name = domain_name
|
98
|
+
@connection_parameters = [
|
99
|
+
aws_key,
|
100
|
+
aws_secret,
|
101
|
+
connection_parameters.merge(:nil_representation => nil_representation)
|
102
|
+
]
|
103
|
+
connect
|
104
|
+
end
|
105
|
+
|
106
|
+
def connect
|
107
|
+
@connection = Aws::SdbInterface.new *@connection_parameters
|
108
|
+
end
|
109
|
+
|
110
|
+
def tables
|
111
|
+
@@collections.keys
|
112
|
+
end
|
113
|
+
|
114
|
+
def columns table_name, name = nil
|
115
|
+
@@collections[table_name].columns
|
116
|
+
end
|
117
|
+
|
118
|
+
def primary_key _
|
119
|
+
'id'
|
120
|
+
end
|
121
|
+
|
122
|
+
def execute sql, name = nil, skip_logging = false
|
123
|
+
log_title = "SimpleDB"
|
124
|
+
log_title += "(batched)" if is_batch sql[:action]
|
125
|
+
log sql.inspect, log_title do
|
126
|
+
case sql[:action]
|
127
|
+
when :insert
|
128
|
+
item_name = get_id sql[:attrs]
|
129
|
+
item_name = sql[:attrs][:id] = generate_id unless item_name
|
130
|
+
if is_batch :update
|
131
|
+
add_to_batch item_name, sql[:attrs], true
|
132
|
+
else
|
133
|
+
@connection.put_attributes domain_name, item_name, sql[:attrs], true
|
134
|
+
end
|
135
|
+
@last_insert_id = item_name
|
136
|
+
when :update
|
137
|
+
item_name = get_id sql[:wheres], true
|
138
|
+
if is_batch :update
|
139
|
+
add_to_batch item_name, sql[:attrs], true
|
140
|
+
else
|
141
|
+
@connection.put_attributes domain_name, item_name, sql[:attrs], true, sql[:wheres]
|
142
|
+
end
|
143
|
+
when :delete
|
144
|
+
item_name = get_id sql[:wheres], true
|
145
|
+
if is_batch :delete
|
146
|
+
add_to_batch item_name
|
147
|
+
else
|
148
|
+
@connection.delete_attributes domain_name, item_name, nil, sql[:wheres]
|
149
|
+
end
|
150
|
+
else
|
151
|
+
raise "Unsupported action: #{sql[:action].inspect}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def insert_sql sql, name = nil, pk = nil, id_value = nil, sequence_name = nil
|
157
|
+
super || @last_insert_id
|
158
|
+
end
|
159
|
+
alias :create :insert_sql
|
160
|
+
|
161
|
+
def select sql, name = nil
|
162
|
+
log sql, "SimpleDB" do
|
163
|
+
result = []
|
164
|
+
response = @connection.select(sql, nil, true)
|
165
|
+
collection_name = get_collection_column_and_name(sql)
|
166
|
+
columns = columns_definition(collection_name)
|
167
|
+
|
168
|
+
response[:items].each do |item|
|
169
|
+
item.each do |id, attrs|
|
170
|
+
ritem = {}
|
171
|
+
ritem['id'] = id unless id == 'Domain' && attrs['Count'] # unless count(*) result
|
172
|
+
attrs.each {|k, vs|
|
173
|
+
column = columns[k]
|
174
|
+
if column.present?
|
175
|
+
ritem[column.name] = column.unquote_number(vs.first)
|
176
|
+
else
|
177
|
+
ritem[k] = vs.first
|
178
|
+
end
|
179
|
+
}
|
180
|
+
result << ritem
|
181
|
+
end
|
182
|
+
end
|
183
|
+
result
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def translate_exception(exception, message)
|
188
|
+
clear_batch
|
189
|
+
raise exception
|
190
|
+
end
|
191
|
+
# Executes the update statement and returns the number of rows affected.
|
192
|
+
def update_sql(sql, name = nil)
|
193
|
+
begin
|
194
|
+
execute(sql, name)
|
195
|
+
1
|
196
|
+
rescue Aws::AwsError => ex
|
197
|
+
#if not a conflict state this raise
|
198
|
+
raise ex if ex.http_code.to_i != 409
|
199
|
+
0
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Executes the delete statement and returns the number of rows affected.
|
204
|
+
def delete_sql(sql, name = nil)
|
205
|
+
update_sql(sql, name)
|
206
|
+
end
|
207
|
+
|
208
|
+
def create_domain domain_name
|
209
|
+
@connection.create_domain domain_name
|
210
|
+
end
|
211
|
+
|
212
|
+
def delete_domain domain_name
|
213
|
+
@connection.delete_domain domain_name
|
214
|
+
end
|
215
|
+
|
216
|
+
def list_domains
|
217
|
+
@connection.list_domains[:domains]
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
def generate_id
|
223
|
+
UUIDTools::UUID.timestamp_create().to_s
|
224
|
+
end
|
225
|
+
|
226
|
+
def get_id hash, delete_id = false
|
227
|
+
if delete_id
|
228
|
+
hash.delete(:id) || hash.delete('id')
|
229
|
+
else
|
230
|
+
hash[:id] || hash['id']
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def get_collection_column_and_name sql
|
235
|
+
if sql.match /`?(#{@@ccn.values.join("|")})`?\s*=\s*'(.*?)'/
|
236
|
+
$2
|
237
|
+
else
|
238
|
+
raise PreparedStatementInvalid, "collection column '#{@@ccn.values.join(" or ")}' not found in the WHERE section in query"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
MAX_BATCH_ITEM_COUNT = 25
|
243
|
+
def batch_pool
|
244
|
+
@batch_pool ||=[]
|
245
|
+
end
|
246
|
+
|
247
|
+
def add_to_batch item_name, attributes = nil, replace = nil
|
248
|
+
batch_pool << Aws::SdbInterface::Item.new(item_name, attributes, replace)
|
249
|
+
if batch_pool.count == MAX_BATCH_ITEM_COUNT
|
250
|
+
type = @batch_type
|
251
|
+
commit_batch
|
252
|
+
begin_batch type
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'aws'
|
2
|
+
module ActiveRecord
|
3
|
+
class Base
|
4
|
+
def self.simpledb_connection(config) # :nodoc:
|
5
|
+
|
6
|
+
config = config.symbolize_keys
|
7
|
+
|
8
|
+
ConnectionAdapters::SimpleDBAdapter.new nil, logger,
|
9
|
+
config[:access_key_id],
|
10
|
+
config[:secret_access_key],
|
11
|
+
config[:domain_name],
|
12
|
+
{
|
13
|
+
:server => config[:host],
|
14
|
+
:port => config[:port],
|
15
|
+
:protocol => config[:protocol],
|
16
|
+
:connection_mode => :per_thread,
|
17
|
+
:logger => SimpleDBLogger.new(logger)
|
18
|
+
},
|
19
|
+
config
|
20
|
+
end
|
21
|
+
|
22
|
+
DEFAULT_COLLECTION_COLUMN_NAME = "collection".freeze
|
23
|
+
|
24
|
+
def self.columns_definition options = {}
|
25
|
+
table_definition = ConnectionAdapters::SimpleDbTableDifinition.new(options[:collection_column_name] || DEFAULT_COLLECTION_COLUMN_NAME)
|
26
|
+
table_definition.primary_key(Base.get_primary_key(table_name.to_s.singularize))
|
27
|
+
|
28
|
+
alias_method_chain :initialize, :defaults
|
29
|
+
|
30
|
+
yield table_definition if block_given?
|
31
|
+
|
32
|
+
ConnectionAdapters::SimpleDBAdapter.set_collection_columns table_name, table_definition
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize_with_defaults(attrs = nil)
|
36
|
+
initialize_without_defaults(attrs) do
|
37
|
+
safe_attribute_names = []
|
38
|
+
if attrs
|
39
|
+
stringified_attrs = attrs.stringify_keys
|
40
|
+
safe_attrs = sanitize_for_mass_assignment(stringified_attrs)
|
41
|
+
safe_attribute_names = safe_attrs.keys.map { |x| x.to_s }
|
42
|
+
end
|
43
|
+
|
44
|
+
ActiveRecord::Base.connection.columns_definition(self.class.table_name).columns_with_defaults.each do |column|
|
45
|
+
if !safe_attribute_names.any? { |attr_name| attr_name =~ /^#{column.name}($|\()/ }
|
46
|
+
value = if column.default.is_a? Proc
|
47
|
+
column.default.call(self)
|
48
|
+
else
|
49
|
+
column.default
|
50
|
+
end
|
51
|
+
__send__("#{column.name}=", value)
|
52
|
+
changed_attributes.delete(column.name)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
yield(self) if block_given?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.batch type = :update, &block
|
60
|
+
connection.begin_batch type
|
61
|
+
block.call
|
62
|
+
connection.commit_batch
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class SimpleDBColumn < Column
|
4
|
+
|
5
|
+
DEFAULT_NUMBER_LIMIT = 4
|
6
|
+
DEFAULT_FLOAT_PRECISION = 4
|
7
|
+
|
8
|
+
def initialize(name, type, limit = nil, pricision = nil, to = nil, default = nil)
|
9
|
+
super name, nil, type, true
|
10
|
+
@limit = limit if limit.present?
|
11
|
+
@precision = precision if precision.present?
|
12
|
+
@default = default
|
13
|
+
@to = to
|
14
|
+
end
|
15
|
+
|
16
|
+
def quote_number value
|
17
|
+
case sql_type
|
18
|
+
when :float then
|
19
|
+
sprintf("%.#{number_precision}f", number_shift + value.to_f)
|
20
|
+
else
|
21
|
+
(number_shift + value.to_i).to_s
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def unquote_number value
|
26
|
+
return nil if value.nil?
|
27
|
+
|
28
|
+
case sql_type
|
29
|
+
when :integer then
|
30
|
+
value.to_i - number_shift
|
31
|
+
when :float then
|
32
|
+
precision_part = 10 ** number_precision
|
33
|
+
((value.to_f - number_shift) * precision_part).round / precision_part.to_f
|
34
|
+
else
|
35
|
+
value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def db_column_name
|
40
|
+
@to || name
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def number_shift
|
45
|
+
5 * 10 ** (limit || DEFAULT_NUMBER_LIMIT)
|
46
|
+
end
|
47
|
+
|
48
|
+
def number_precision
|
49
|
+
@precision || DEFAULT_FLOAT_PRECISION
|
50
|
+
end
|
51
|
+
|
52
|
+
def simplified_type(field_type)
|
53
|
+
t = field_type.to_s
|
54
|
+
if t == "primary_key"
|
55
|
+
:string
|
56
|
+
else
|
57
|
+
super(t)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module FinderMethods
|
3
|
+
def exists?(id = nil)
|
4
|
+
id = id.id if ActiveRecord::Base === id
|
5
|
+
|
6
|
+
join_dependency = construct_join_dependency_for_association_find
|
7
|
+
relation = construct_relation_for_association_find(join_dependency)
|
8
|
+
relation = relation.except(:select).select("*").limit(1)
|
9
|
+
|
10
|
+
case id
|
11
|
+
when Array, Hash
|
12
|
+
relation = relation.where(id)
|
13
|
+
else
|
14
|
+
relation = relation.where(table[primary_key.name].eq(id)) if id
|
15
|
+
end
|
16
|
+
|
17
|
+
connection.select_value(relation.to_sql) ? true : false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class Aws::SdbInterface
|
2
|
+
def put_attributes(domain_name, item_name, attributes, replace = false, expected_attributes = {})
|
3
|
+
params = params_with_attributes(domain_name, item_name, attributes, replace, expected_attributes)
|
4
|
+
link = generate_request("PutAttributes", params)
|
5
|
+
request_info( link, QSdbSimpleParser.new )
|
6
|
+
rescue Exception
|
7
|
+
on_exception
|
8
|
+
end
|
9
|
+
|
10
|
+
def delete_attributes(domain_name, item_name, attributes = nil, expected_attributes = {})
|
11
|
+
params = params_with_attributes(domain_name, item_name, attributes, false, expected_attributes)
|
12
|
+
link = generate_request("DeleteAttributes", params)
|
13
|
+
request_info( link, QSdbSimpleParser.new )
|
14
|
+
rescue Exception
|
15
|
+
on_exception
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def pack_expected_attributes(attributes) #:nodoc:
|
20
|
+
{}.tap do |result|
|
21
|
+
idx = 0
|
22
|
+
attributes.each do |attribute, value|
|
23
|
+
v = value.is_a?(Array) ? value.first : value
|
24
|
+
result["Expected.#{idx}.Name"] = attribute.to_s
|
25
|
+
result["Expected.#{idx}.Value"] = ruby_to_sdb(v)
|
26
|
+
idx += 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def pack_attributes(attributes = {}, replace = false, key_prefix = "")
|
32
|
+
{}.tap do |result|
|
33
|
+
idx = 0
|
34
|
+
if attributes
|
35
|
+
attributes.each do |attribute, value|
|
36
|
+
v = value.is_a?(Array) ? value.first : value
|
37
|
+
result["#{key_prefix}Attribute.#{idx}.Replace"] = 'true' if replace
|
38
|
+
result["#{key_prefix}Attribute.#{idx}.Name"] = attribute
|
39
|
+
result["#{key_prefix}Attribute.#{idx}.Value"] = ruby_to_sdb(v)
|
40
|
+
idx += 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def params_with_attributes(domain_name, item_name, attributes, replace, expected_attrubutes)
|
47
|
+
{}.tap do |p|
|
48
|
+
p['DomainName'] = domain_name
|
49
|
+
p['ItemName'] = item_name
|
50
|
+
p.merge!(pack_attributes(attributes, replace)).merge!(pack_expected_attributes(expected_attrubutes))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|