datamapper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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