activerecord-simpledb-adapter 0.2.1 → 0.3.0
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.
- 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
|