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