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.
@@ -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