datamapper 0.1.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.
Files changed (85) hide show
  1. data/CHANGELOG +2 -0
  2. data/MIT-LICENSE +22 -0
  3. data/README +1 -0
  4. data/example.rb +25 -0
  5. data/lib/data_mapper.rb +30 -0
  6. data/lib/data_mapper/adapters/abstract_adapter.rb +229 -0
  7. data/lib/data_mapper/adapters/mysql_adapter.rb +171 -0
  8. data/lib/data_mapper/adapters/sqlite3_adapter.rb +189 -0
  9. data/lib/data_mapper/associations.rb +19 -0
  10. data/lib/data_mapper/associations/belongs_to_association.rb +111 -0
  11. data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +100 -0
  12. data/lib/data_mapper/associations/has_many_association.rb +101 -0
  13. data/lib/data_mapper/associations/has_one_association.rb +107 -0
  14. data/lib/data_mapper/base.rb +160 -0
  15. data/lib/data_mapper/callbacks.rb +47 -0
  16. data/lib/data_mapper/database.rb +134 -0
  17. data/lib/data_mapper/extensions/active_record_impersonation.rb +69 -0
  18. data/lib/data_mapper/extensions/callback_helpers.rb +35 -0
  19. data/lib/data_mapper/identity_map.rb +21 -0
  20. data/lib/data_mapper/loaded_set.rb +45 -0
  21. data/lib/data_mapper/mappings/column.rb +78 -0
  22. data/lib/data_mapper/mappings/schema.rb +28 -0
  23. data/lib/data_mapper/mappings/table.rb +99 -0
  24. data/lib/data_mapper/queries/conditions.rb +141 -0
  25. data/lib/data_mapper/queries/connection.rb +34 -0
  26. data/lib/data_mapper/queries/create_table_statement.rb +38 -0
  27. data/lib/data_mapper/queries/delete_statement.rb +17 -0
  28. data/lib/data_mapper/queries/drop_table_statement.rb +17 -0
  29. data/lib/data_mapper/queries/insert_statement.rb +29 -0
  30. data/lib/data_mapper/queries/reader.rb +42 -0
  31. data/lib/data_mapper/queries/result.rb +19 -0
  32. data/lib/data_mapper/queries/select_statement.rb +103 -0
  33. data/lib/data_mapper/queries/table_exists_statement.rb +17 -0
  34. data/lib/data_mapper/queries/truncate_table_statement.rb +17 -0
  35. data/lib/data_mapper/queries/update_statement.rb +25 -0
  36. data/lib/data_mapper/session.rb +240 -0
  37. data/lib/data_mapper/support/blank_slate.rb +3 -0
  38. data/lib/data_mapper/support/connection_pool.rb +117 -0
  39. data/lib/data_mapper/support/enumerable.rb +27 -0
  40. data/lib/data_mapper/support/inflector.rb +329 -0
  41. data/lib/data_mapper/support/proc.rb +69 -0
  42. data/lib/data_mapper/support/string.rb +23 -0
  43. data/lib/data_mapper/support/symbol.rb +91 -0
  44. data/lib/data_mapper/support/weak_hash.rb +46 -0
  45. data/lib/data_mapper/unit_of_work.rb +38 -0
  46. data/lib/data_mapper/validations/confirmation_validator.rb +55 -0
  47. data/lib/data_mapper/validations/contextual_validations.rb +50 -0
  48. data/lib/data_mapper/validations/format_validator.rb +85 -0
  49. data/lib/data_mapper/validations/formats/email.rb +78 -0
  50. data/lib/data_mapper/validations/generic_validator.rb +27 -0
  51. data/lib/data_mapper/validations/length_validator.rb +75 -0
  52. data/lib/data_mapper/validations/required_field_validator.rb +47 -0
  53. data/lib/data_mapper/validations/unique_validator.rb +65 -0
  54. data/lib/data_mapper/validations/validation_errors.rb +34 -0
  55. data/lib/data_mapper/validations/validation_helper.rb +60 -0
  56. data/performance.rb +156 -0
  57. data/profile_data_mapper.rb +18 -0
  58. data/rakefile.rb +80 -0
  59. data/spec/basic_finder.rb +67 -0
  60. data/spec/belongs_to.rb +47 -0
  61. data/spec/fixtures/animals.yaml +32 -0
  62. data/spec/fixtures/exhibits.yaml +90 -0
  63. data/spec/fixtures/fruit.yaml +6 -0
  64. data/spec/fixtures/people.yaml +15 -0
  65. data/spec/fixtures/zoos.yaml +20 -0
  66. data/spec/has_and_belongs_to_many.rb +25 -0
  67. data/spec/has_many.rb +34 -0
  68. data/spec/legacy.rb +14 -0
  69. data/spec/models/animal.rb +7 -0
  70. data/spec/models/exhibit.rb +6 -0
  71. data/spec/models/fruit.rb +6 -0
  72. data/spec/models/person.rb +7 -0
  73. data/spec/models/post.rb +4 -0
  74. data/spec/models/sales_person.rb +4 -0
  75. data/spec/models/zoo.rb +5 -0
  76. data/spec/new_record.rb +24 -0
  77. data/spec/spec_helper.rb +61 -0
  78. data/spec/sub_select.rb +16 -0
  79. data/spec/symbolic_operators.rb +21 -0
  80. data/spec/validates_confirmation_of.rb +36 -0
  81. data/spec/validates_format_of.rb +61 -0
  82. data/spec/validates_length_of.rb +101 -0
  83. data/spec/validates_uniqueness_of.rb +45 -0
  84. data/spec/validations.rb +63 -0
  85. metadata +134 -0
@@ -0,0 +1,34 @@
1
+ require File.dirname(__FILE__) + '/result'
2
+ require File.dirname(__FILE__) + '/reader'
3
+
4
+ module DataMapper
5
+ module Queries
6
+
7
+ class Connection
8
+
9
+ def initialize(logger)
10
+ @logger = logger
11
+ end
12
+
13
+ def log
14
+ @logger
15
+ end
16
+
17
+ def execute(sql)
18
+ raise NotImplementedError.new
19
+ Results.new
20
+ end
21
+
22
+ def query(sql)
23
+ raise NotImplementedError.new
24
+ Reader.new
25
+ end
26
+
27
+ def close
28
+ raise NotImplementedError.new
29
+ end
30
+
31
+ end
32
+
33
+ end # module Queries
34
+ end # module DataMapper
@@ -0,0 +1,38 @@
1
+ module DataMapper
2
+ module Queries
3
+
4
+ class CreateTableStatement
5
+
6
+ def initialize(database, klass)
7
+ @database, @klass = database, klass
8
+ end
9
+
10
+ def to_sql
11
+ table = @database[@klass]
12
+
13
+ sql = "CREATE TABLE " << table.to_sql << " ("
14
+
15
+ sql << table.columns.map do |column|
16
+ column_long_form(column)
17
+ end.join(', ')
18
+
19
+ sql << ", PRIMARY KEY (#{table.key.to_sql}))"
20
+
21
+ return sql
22
+ end
23
+
24
+ def column_long_form(column)
25
+ long_form = "#{column.to_sql} #{@database.adapter.class::TYPES[column.type] || column.type}"
26
+
27
+ long_form << "(#{column.size})" unless column.size.nil?
28
+ long_form << " NOT NULL" unless column.nullable?
29
+ long_form << " " << @database.syntax(:auto_increment) if column.key?
30
+ long_form << " default #{column.options[:default]}" if column.options.has_key?(:default)
31
+
32
+ return long_form
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ module DataMapper
2
+ module Queries
3
+
4
+ class DeleteStatement
5
+
6
+ def initialize(database, instance)
7
+ @database, @instance = database, instance
8
+ end
9
+
10
+ def to_sql
11
+ "DELETE FROM " << @database[@instance.class].to_sql << " WHERE id = " << @database.quote_value(@instance.key)
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module DataMapper
2
+ module Queries
3
+
4
+ class DropTableStatement
5
+
6
+ def initialize(database, klass)
7
+ @database, @klass = database, klass
8
+ end
9
+
10
+ def to_sql
11
+ "DROP TABLE #{@database[@klass].to_sql}"
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ module DataMapper
2
+ module Queries
3
+
4
+ class InsertStatement
5
+
6
+ def initialize(database, instance)
7
+ @database, @instance = database, instance
8
+ end
9
+
10
+ # The only thing this method is responsible for is generating the insert statement.
11
+ # It is the database adapters responsibility to get the last inserted id
12
+ def to_sql
13
+
14
+ table = @database[@instance.class]
15
+
16
+ keys = []
17
+ values = []
18
+
19
+ @instance.dirty_attributes.each_pair { |k,v| keys << table[k].to_sql; values << v }
20
+
21
+ # Formatting is a bit off here, but it looks nicer in the log this way.
22
+ sql = "INSERT INTO #{table.to_sql} (#{keys.join(', ')}) \
23
+ VALUES (#{values.map { |v| @database.quote_value(v) }.join(', ')})"
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ module DataMapper
2
+ module Queries
3
+
4
+ class Reader
5
+
6
+ attr_reader :columns
7
+
8
+ def initialize(database_result_set)
9
+ end
10
+
11
+ def eof?
12
+ raise NotImplementedError.new
13
+ end
14
+
15
+ def records_affected
16
+ raise NotImplementedError.new
17
+ end
18
+
19
+ def each
20
+ raise NotImplementedError.new
21
+ end
22
+
23
+ def entries
24
+ raise NotImplementedError.new
25
+ end
26
+
27
+ def [](column)
28
+ raise NotImplementedError.new
29
+ end
30
+
31
+ def each_pair
32
+ raise NotImplementedError.new
33
+ end
34
+
35
+ def close
36
+ raise NotImplementedError.new
37
+ end
38
+
39
+ end
40
+
41
+ end # module Queries
42
+ end # module DataMapper
@@ -0,0 +1,19 @@
1
+ module DataMapper
2
+ module Queries
3
+
4
+ class Result
5
+
6
+ attr_accessor :size, :last_inserted_id
7
+
8
+ def initialize(size, last_inserted_id = nil)
9
+ @size, @last_inserted_id = size, last_inserted_id
10
+ end
11
+
12
+ def success?
13
+ size > 0
14
+ end
15
+
16
+ end # class Result
17
+
18
+ end # module Queries
19
+ end # module DataMapper
@@ -0,0 +1,103 @@
1
+ require 'data_mapper/queries/conditions'
2
+
3
+ module DataMapper
4
+ module Queries
5
+
6
+ class SelectStatement
7
+
8
+ def initialize(database, options)
9
+ @database, @options = database, options
10
+ end
11
+
12
+ def limit
13
+ @options[:limit]
14
+ end
15
+
16
+ def order
17
+ @options[:order]
18
+ end
19
+
20
+ def klass
21
+ @options[:class]
22
+ end
23
+
24
+ def has_id?
25
+ conditions.has_id?
26
+ end
27
+
28
+ def escape(conditions)
29
+ @database.escape(conditions)
30
+ end
31
+
32
+ def inspect
33
+ @options.inspect
34
+ end
35
+
36
+ def include?(association_name)
37
+ return false if includes.empty?
38
+ includes.include?(association_name)
39
+ end
40
+
41
+ def includes
42
+ list = @options[:include] ||= []
43
+ list.kind_of?(Array) ? list : [list]
44
+ end
45
+
46
+ def reload?
47
+ @options[:reload]
48
+ end
49
+
50
+ def select
51
+ select_columns = @options[:select]
52
+ unless select_columns.nil?
53
+ select_columns = select_columns.kind_of?(Array) ? select_columns : (@options[:select] = [select_columns])
54
+ select_columns.map { |column| @database.quote_column_name(column.to_s) }
55
+ else
56
+ @options[:select] = @database[klass].columns.select do |column|
57
+ include?(column.name) || !column.lazy?
58
+ end.map { |column| column.to_sql }
59
+ end
60
+ end
61
+
62
+ def instance_id
63
+ @options[:id]
64
+ end
65
+
66
+ def conditions
67
+ @conditions ||= Conditions.new(@database, @options)
68
+ end
69
+
70
+ def table
71
+ @table_name || @table_name = if @options.has_key?(:table)
72
+ @database.quote_table_name(@options[:table])
73
+ else
74
+ @database[klass].to_sql
75
+ end
76
+ end
77
+
78
+ def to_sql
79
+ sql = 'SELECT ' << select.join(', ') << ' FROM ' << table
80
+
81
+ where = []
82
+
83
+ where += conditions.to_a unless conditions.empty?
84
+
85
+ unless where.empty?
86
+ sql << ' WHERE (' << where.join(') AND (') << ')'
87
+ end
88
+
89
+ unless order.nil?
90
+ sql << ' ORDER BY ' << order.to_s
91
+ end
92
+
93
+ unless limit.nil?
94
+ sql << ' LIMIT ' << limit.to_s
95
+ end
96
+
97
+ return sql
98
+ end
99
+
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,17 @@
1
+ module DataMapper
2
+ module Queries
3
+
4
+ class TableExistsStatement
5
+
6
+ def initialize(database, klass)
7
+ @database, @klass = database, klass
8
+ end
9
+
10
+ def to_sql
11
+ "SHOW TABLES LIKE #{@database.quote_value(@database[@klass].name)}"
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module DataMapper
2
+ module Queries
3
+
4
+ class TruncateTableStatement
5
+
6
+ def initialize(database, klass)
7
+ @database, @klass = database, klass
8
+ end
9
+
10
+ def to_sql
11
+ "TRUNCATE TABLE " << @database[@klass].to_sql
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ module DataMapper
2
+ module Queries
3
+
4
+ class UpdateStatement
5
+
6
+ def initialize(database, instance)
7
+ @database, @instance = database, instance
8
+ end
9
+
10
+ def to_sql
11
+ table = @database[@instance.class]
12
+
13
+ sql = "UPDATE " << table.to_sql << " SET "
14
+
15
+ @instance.dirty_attributes.map do |k, v|
16
+ sql << table[k].to_sql << " = " << @database.quote_value(v) << ", "
17
+ end
18
+
19
+ sql[0, sql.size - 2] << " WHERE #{table.key.to_sql} = " << @database.quote_value(@instance.key)
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,240 @@
1
+ require 'data_mapper/loaded_set'
2
+ require 'data_mapper/identity_map'
3
+
4
+ module DataMapper
5
+
6
+ class Session
7
+
8
+ FIND_OPTIONS = [
9
+ :select, :limit, :class, :include, :reload, :conditions, :order
10
+ ]
11
+
12
+ class MaterializationError < StandardError
13
+ end
14
+
15
+ def initialize(database)
16
+ @database = database
17
+ end
18
+
19
+ def identity_map
20
+ @identity_map || ( @identity_map = IdentityMap.new )
21
+ end
22
+
23
+ def find(klass, type_or_id, options = {}, &b)
24
+ options.merge! b.to_hash if block_given?
25
+
26
+ results = case type_or_id
27
+ when :first then
28
+ first(@database.select_statement(options.merge(:class => klass, :limit => 1)))
29
+ when :all then
30
+ all(@database.select_statement(options.merge(:class => klass)))
31
+ else
32
+ first(@database.select_statement(options.merge(:class => klass, :id => type_or_id)))
33
+ end
34
+
35
+ case results
36
+ when Array then results.each { |instance| instance.session = self }
37
+ when Base then results.session = self
38
+ end
39
+ return results
40
+ end
41
+
42
+ def first(options)
43
+ if options.has_id? && !options.reload?
44
+ instance = identity_map.get(options.klass, options.instance_id)
45
+ return instance unless instance.nil?
46
+ end
47
+
48
+ reader = @database.query(options)
49
+ instance = reader.eof? ? nil : load(options, reader.next)
50
+ reader.close
51
+ return instance
52
+ rescue DatabaseError => de
53
+ de.options = options
54
+ raise de
55
+ end
56
+
57
+ def all(options)
58
+ set = LoadedSet.new(@database)
59
+ reader = @database.query(options)
60
+ instances = reader.map do |hash|
61
+ load(options, hash, set)
62
+ end
63
+ reader.close
64
+ return instances
65
+ rescue => error
66
+ @database.log.error(error)
67
+ raise error
68
+ end
69
+
70
+ def load(options, hash, set = LoadedSet.new(@database))
71
+
72
+ instance_class = unless hash['type'].nil?
73
+ Kernel::const_get(hash['type'])
74
+ else
75
+ options.klass
76
+ end
77
+
78
+ mapping = @database[instance_class]
79
+
80
+ instance_id = mapping.key.type_cast_value(hash['id'])
81
+ instance = identity_map.get(instance_class, instance_id)
82
+
83
+ if instance.nil? || options.reload?
84
+ instance ||= instance_class.new
85
+ instance.class.callbacks.execute(:before_materialize, instance)
86
+
87
+ instance.instance_variable_set(:@new_record, false)
88
+ hash.each_pair do |name_as_string,raw_value|
89
+ name = name_as_string.to_sym
90
+ if column = mapping.find_by_column_name(name)
91
+ value = column.type_cast_value(raw_value)
92
+ instance.instance_variable_set(column.instance_variable_name, value)
93
+ else
94
+ instance.instance_variable_set("@#{name}", value)
95
+ end
96
+ instance.original_hashes[name] = value.hash
97
+ end
98
+
99
+ instance.class.callbacks.execute(:after_materialize, instance)
100
+
101
+ identity_map.set(instance)
102
+ end
103
+
104
+ instance.instance_variable_set(:@loaded_set, set)
105
+ set.instances << instance
106
+ return instance
107
+ end
108
+
109
+ def save(instance)
110
+ return false unless instance.dirty?
111
+ instance.class.callbacks.execute(:before_save, instance)
112
+ result = instance.new_record? ? insert(instance) : update(instance)
113
+ instance.session = self
114
+ instance.class.callbacks.execute(:after_save, instance)
115
+ result.success?
116
+ end
117
+
118
+ def insert(instance, inserted_id = nil)
119
+ instance.class.callbacks.execute(:before_create, instance)
120
+ result = @database.execute(@database.insert_statement(instance))
121
+
122
+ if result.success?
123
+ instance.instance_variable_set(:@new_record, false)
124
+ instance.instance_variable_set(:@id, inserted_id || result.last_inserted_id)
125
+ calculate_original_hashes(instance)
126
+ identity_map.set(instance)
127
+ instance.class.callbacks.execute(:after_create, instance)
128
+ end
129
+
130
+ return result
131
+ rescue => error
132
+ @database.log.error(error)
133
+ raise error
134
+ end
135
+
136
+ def update(instance)
137
+ instance.class.callbacks.execute(:before_update, instance)
138
+ result = @database.execute(@database.update_statement(instance))
139
+ calculate_original_hashes(instance)
140
+ instance.class.callbacks.execute(:after_update, instance)
141
+ return result
142
+ rescue => error
143
+ @database.log.error(error)
144
+ raise error
145
+ end
146
+
147
+ def destroy(instance)
148
+ instance.class.callbacks.execute(:before_destroy, instance)
149
+ result = @database.execute(@database.delete_statement(instance))
150
+ if result.success?
151
+ instance.instance_variable_set(:@new_record, true)
152
+ instance.original_hashes.clear
153
+ instance.class.callbacks.execute(:after_destroy, instance)
154
+ end
155
+ return result.success?
156
+ rescue => error
157
+ @database.log.error(error)
158
+ raise error
159
+ end
160
+
161
+ def delete_all(klass)
162
+ @database.execute(@database.delete_statement(klass))
163
+ end
164
+
165
+ def truncate(klass)
166
+ @database.connection do |db|
167
+ db.execute(@database.truncate_table_statement(klass))
168
+ end
169
+ end
170
+
171
+ def create_table(klass)
172
+ @database.connection do |db|
173
+ db.execute(@database.create_table_statement(klass))
174
+ end unless table_exists?(klass)
175
+ end
176
+
177
+ def drop_table(klass)
178
+ @database.connection do |db|
179
+ db.execute(@database.drop_table_statement(klass))
180
+ end if table_exists?(klass)
181
+ end
182
+
183
+ def table_exists?(klass)
184
+ reader = @database.connection do |db|
185
+ db.query(@database.table_exists_statement(klass))
186
+ end
187
+ result = !reader.eof?
188
+ reader.close
189
+ result
190
+ end
191
+
192
+ def query(*args)
193
+ sql = args.shift
194
+
195
+ unless args.empty?
196
+ sql.gsub!(/\?/) do |x|
197
+ @database.quote_value(args.shift)
198
+ end
199
+ end
200
+
201
+ reader = @database.connection do |db|
202
+ db.query(sql)
203
+ end
204
+
205
+ columns = reader.columns.keys
206
+ klass = Struct.new(*columns.map { |c| c.to_sym })
207
+
208
+ rows = reader.map do |row|
209
+ klass.new(*columns.map { |c| row[c] })
210
+ end
211
+
212
+ reader.close
213
+ return rows
214
+ end
215
+
216
+ def schema
217
+ @database.schema
218
+ end
219
+
220
+ def log
221
+ @database.log
222
+ end
223
+
224
+ private
225
+
226
+ # Make sure this uses the factory changes later...
227
+ def type_cast_value(klass, name, raw_value)
228
+ @database[klass][name].type_cast_value(raw_value)
229
+ end
230
+
231
+ # Calculates the original hashes for each value
232
+ # in an instance's set of attributes, and adds
233
+ # them to the original_hashes hash.
234
+ def calculate_original_hashes(instance)
235
+ instance.attributes.each_pair do |name, value|
236
+ instance.original_hashes[name] = value.hash
237
+ end
238
+ end
239
+ end
240
+ end