datamapper 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/CHANGELOG +31 -1
  2. data/MIT-LICENSE +1 -1
  3. data/README +9 -1
  4. data/example.rb +23 -15
  5. data/lib/data_mapper.rb +5 -0
  6. data/lib/data_mapper/adapters/abstract_adapter.rb +9 -207
  7. data/lib/data_mapper/adapters/mysql_adapter.rb +132 -108
  8. data/lib/data_mapper/adapters/postgresql_adapter.rb +242 -0
  9. data/lib/data_mapper/adapters/sql/coersion.rb +74 -0
  10. data/lib/data_mapper/adapters/sql/commands/advanced_load_command.rb +140 -0
  11. data/lib/data_mapper/adapters/sql/commands/conditions.rb +161 -0
  12. data/lib/data_mapper/adapters/sql/commands/delete_command.rb +113 -0
  13. data/lib/data_mapper/adapters/sql/commands/load_command.rb +296 -0
  14. data/lib/data_mapper/adapters/sql/commands/save_command.rb +141 -0
  15. data/lib/data_mapper/adapters/sql/commands/table_exists_command.rb +33 -0
  16. data/lib/data_mapper/adapters/sql/mappings/column.rb +91 -0
  17. data/lib/data_mapper/adapters/sql/mappings/schema.rb +30 -0
  18. data/lib/data_mapper/adapters/sql/mappings/table.rb +143 -0
  19. data/lib/data_mapper/adapters/sql/quoting.rb +38 -0
  20. data/lib/data_mapper/adapters/sql_adapter.rb +163 -0
  21. data/lib/data_mapper/adapters/sqlite3_adapter.rb +155 -116
  22. data/lib/data_mapper/associations.rb +2 -0
  23. data/lib/data_mapper/associations/advanced_has_many_association.rb +55 -0
  24. data/lib/data_mapper/associations/belongs_to_association.rb +2 -2
  25. data/lib/data_mapper/associations/has_many_association.rb +3 -3
  26. data/lib/data_mapper/associations/has_one_association.rb +2 -2
  27. data/lib/data_mapper/base.rb +30 -11
  28. data/lib/data_mapper/callbacks.rb +4 -1
  29. data/lib/data_mapper/database.rb +8 -41
  30. data/lib/data_mapper/identity_map.rb +23 -3
  31. data/lib/data_mapper/session.rb +34 -186
  32. data/lib/data_mapper/{extensions → support}/active_record_impersonation.rb +16 -12
  33. data/lib/data_mapper/support/blank.rb +35 -0
  34. data/lib/data_mapper/support/connection_pool.rb +2 -1
  35. data/lib/data_mapper/support/string.rb +27 -0
  36. data/lib/data_mapper/support/struct.rb +26 -0
  37. data/lib/data_mapper/validations/unique_validator.rb +1 -3
  38. data/lib/data_mapper/validations/validation_helper.rb +1 -1
  39. data/performance.rb +24 -7
  40. data/profile_data_mapper.rb +24 -2
  41. data/rakefile.rb +2 -2
  42. data/spec/basic_finder.rb +2 -2
  43. data/spec/belongs_to.rb +1 -1
  44. data/spec/delete_command_spec.rb +9 -0
  45. data/spec/fixtures/zoos.yaml +4 -0
  46. data/spec/has_many.rb +1 -1
  47. data/spec/load_command_spec.rb +44 -0
  48. data/spec/models/zoo.rb +2 -0
  49. data/spec/save_command_spec.rb +13 -0
  50. data/spec/spec_helper.rb +10 -1
  51. data/spec/support/string_spec.rb +7 -0
  52. data/spec/validates_confirmation_of.rb +1 -1
  53. data/spec/validates_format_of.rb +1 -1
  54. data/spec/validates_length_of.rb +1 -1
  55. data/spec/validations.rb +1 -1
  56. metadata +23 -20
  57. data/lib/data_mapper/extensions/callback_helpers.rb +0 -35
  58. data/lib/data_mapper/loaded_set.rb +0 -45
  59. data/lib/data_mapper/mappings/column.rb +0 -78
  60. data/lib/data_mapper/mappings/schema.rb +0 -28
  61. data/lib/data_mapper/mappings/table.rb +0 -99
  62. data/lib/data_mapper/queries/conditions.rb +0 -141
  63. data/lib/data_mapper/queries/connection.rb +0 -34
  64. data/lib/data_mapper/queries/create_table_statement.rb +0 -38
  65. data/lib/data_mapper/queries/delete_statement.rb +0 -17
  66. data/lib/data_mapper/queries/drop_table_statement.rb +0 -17
  67. data/lib/data_mapper/queries/insert_statement.rb +0 -29
  68. data/lib/data_mapper/queries/reader.rb +0 -42
  69. data/lib/data_mapper/queries/result.rb +0 -19
  70. data/lib/data_mapper/queries/select_statement.rb +0 -103
  71. data/lib/data_mapper/queries/table_exists_statement.rb +0 -17
  72. data/lib/data_mapper/queries/truncate_table_statement.rb +0 -17
  73. data/lib/data_mapper/queries/update_statement.rb +0 -25
@@ -0,0 +1,163 @@
1
+ require 'data_mapper/adapters/abstract_adapter'
2
+ require 'data_mapper/adapters/sql/commands/load_command'
3
+ require 'data_mapper/adapters/sql/commands/advanced_load_command'
4
+ require 'data_mapper/adapters/sql/commands/save_command'
5
+ require 'data_mapper/adapters/sql/commands/delete_command'
6
+ require 'data_mapper/adapters/sql/commands/table_exists_command'
7
+ require 'data_mapper/adapters/sql/coersion'
8
+ require 'data_mapper/adapters/sql/quoting'
9
+ require 'data_mapper/adapters/sql/mappings/schema'
10
+
11
+ module DataMapper
12
+
13
+ # An Adapter is really a Factory for three types of object,
14
+ # so they can be selectively sub-classed where needed.
15
+ #
16
+ # The first type is a Query. The Query is an object describing
17
+ # the database-specific operations we wish to perform, in an
18
+ # abstract manner. For example: While most if not all databases
19
+ # support a mechanism for limiting the size of results returned,
20
+ # some use a "LIMIT" keyword, while others use a "TOP" keyword.
21
+ # We can set a SelectStatement#limit field then, and allow
22
+ # the adapter to override the underlying SQL generated.
23
+ # Refer to DataMapper::Queries.
24
+ #
25
+ # The second type provided by the Adapter is a DataMapper::Connection.
26
+ # This allows us to execute queries and return results in a clear and
27
+ # uniform manner we can use throughout the DataMapper.
28
+ #
29
+ # The final type provided is a DataMapper::Transaction.
30
+ # Transactions are duck-typed Connections that span multiple queries.
31
+ #
32
+ # Note: It is assumed that the Adapter implements it's own
33
+ # ConnectionPool if any since some libraries implement their own at
34
+ # a low-level, and it wouldn't make sense to pay a performance
35
+ # cost twice by implementing a secondary pool in the DataMapper itself.
36
+ # If the library being adapted does not provide such functionality,
37
+ # DataMapper::Support::ConnectionPool can be used.
38
+ module Adapters
39
+
40
+ # You must inherit from the SqlAdapter, and implement the
41
+ # required methods to adapt a database library for use with the DataMapper.
42
+ #
43
+ # NOTE: By inheriting from SqlAdapter, you get a copy of all the
44
+ # standard sub-modules (Quoting, Coersion and Queries) in your own Adapter.
45
+ # You can extend and overwrite these copies without affecting the originals.
46
+ class SqlAdapter < AbstractAdapter
47
+
48
+ FIND_OPTIONS = [
49
+ :select, :limit, :class, :include, :reload, :conditions, :order
50
+ ]
51
+
52
+ def initialize(configuration)
53
+ super
54
+ @single_threaded = configuration.single_threaded
55
+ end
56
+
57
+ def single_threaded?
58
+ @single_threaded
59
+ end
60
+
61
+ def connection(&block)
62
+ raise NotImplementedError.new
63
+ end
64
+
65
+ def transaction(&block)
66
+ raise NotImplementedError.new
67
+ end
68
+
69
+ def query(sql)
70
+ raise NotImplementedError.new
71
+ end
72
+
73
+ def schema
74
+ @schema || ( @schema = Mappings::Schema.new(self) )
75
+ end
76
+
77
+ def table_exists?(name)
78
+ self.class::Commands::TableExistsCommand.new(self, name).call
79
+ end
80
+
81
+ def delete(klass_or_instance, options = nil)
82
+ self.class::Commands::DeleteCommand.new(self, klass_or_instance, options).call
83
+ end
84
+
85
+ def save(session, instance)
86
+ self.class::Commands::SaveCommand.new(self, session, instance).call
87
+ end
88
+
89
+ def load(session, klass, options)
90
+ self.class::Commands::LoadCommand.new(self, session, klass, options).call
91
+ end
92
+
93
+ def [](klass_or_table_name)
94
+ schema[klass_or_table_name]
95
+ end
96
+
97
+ # Escape a string of SQL with a set of arguments.
98
+ # The first argument is assumed to be the SQL to escape,
99
+ # the remaining arguments (if any) are assumed to be
100
+ # values to escape and interpolate.
101
+ #
102
+ # ==== Examples
103
+ # escape_sql("SELECT * FROM zoos")
104
+ # # => "SELECT * FROM zoos"
105
+ #
106
+ # escape_sql("SELECT * FROM zoos WHERE name = ?", "Dallas")
107
+ # # => "SELECT * FROM zoos WHERE name = `Dallas`"
108
+ #
109
+ # escape_sql("SELECT * FROM zoos WHERE name = ? AND acreage > ?", "Dallas", 40)
110
+ # # => "SELECT * FROM zoos WHERE name = `Dallas` AND acreage > 40"
111
+ #
112
+ # ==== Warning
113
+ # This method is meant mostly for adapters that don't support
114
+ # bind-parameters.
115
+ def escape_sql(*args)
116
+ sql = args.shift
117
+
118
+ unless args.empty?
119
+ sql.gsub!(/\?/) do |x|
120
+ quote_value(args.shift)
121
+ end
122
+ end
123
+
124
+ sql
125
+ end
126
+
127
+ # This callback copies and sub-classes modules and classes
128
+ # in the AbstractAdapter to the inherited class so you don't
129
+ # have to copy and paste large blocks of code from the
130
+ # SqlAdapter.
131
+ #
132
+ # Basically, when inheriting from the AbstractAdapter, you
133
+ # aren't just inheriting a single class, you're inheriting
134
+ # a whole graph of Types. For convenience.
135
+ def self.inherited(base)
136
+
137
+ queries = base.const_set('Commands', Module.new)
138
+
139
+ Sql::Commands.constants.each do |name|
140
+ queries.const_set(name, Class.new(Sql::Commands.const_get(name)))
141
+ end
142
+
143
+ base.const_set('TYPES', TYPES.dup)
144
+ base.const_set('FIND_OPTIONS', FIND_OPTIONS.dup)
145
+
146
+ super
147
+ end
148
+
149
+ TYPES = {
150
+ :integer => 'int'.freeze,
151
+ :string => 'varchar'.freeze,
152
+ :text => 'text'.freeze,
153
+ :class => 'varchar'.freeze
154
+ }
155
+
156
+ include Sql
157
+ include Quoting
158
+ include Coersion
159
+
160
+ end # class SqlAdapter
161
+
162
+ end # module Adapters
163
+ end # module DataMapper
@@ -1,4 +1,4 @@
1
- require 'data_mapper/adapters/abstract_adapter'
1
+ require 'data_mapper/adapters/sql_adapter'
2
2
  require 'data_mapper/support/connection_pool'
3
3
 
4
4
  require 'sqlite3'
@@ -6,11 +6,15 @@ require 'sqlite3'
6
6
  module DataMapper
7
7
  module Adapters
8
8
 
9
- class Sqlite3Adapter < AbstractAdapter
9
+ class Sqlite3Adapter < SqlAdapter
10
10
 
11
11
  def initialize(configuration)
12
12
  super
13
- @connections = Support::ConnectionPool.new { Queries::Connection.new(@configuration) }
13
+ @connections = Support::ConnectionPool.new do
14
+ dbh = SQLite3::Database.new(configuration.database)
15
+ dbh.results_as_hash = true
16
+ dbh
17
+ end
14
18
  end
15
19
 
16
20
  def connection
@@ -34,126 +38,76 @@ module DataMapper
34
38
  end
35
39
  end
36
40
 
41
+ def query(*args)
42
+ reader = connection { |db| db.query(escape_sql(*args)) }
43
+
44
+ fields = nil
45
+ rows = []
46
+
47
+ until reader.eof?
48
+ hash = reader.next
49
+ break if hash.nil?
50
+
51
+ fields = hash.keys.select { |field| field.is_a?(String) } unless fields
52
+
53
+ rows << fields.map { |field| hash[field] }
54
+ end
55
+
56
+ reader.close
57
+
58
+ struct = Support::Struct::define(fields)
59
+
60
+ rows.map do |row|
61
+ struct.new(row)
62
+ end
63
+ end
64
+
37
65
  TYPES.merge!({
38
66
  :integer => 'INTEGER'.freeze,
39
67
  :string => 'TEXT'.freeze,
40
68
  :text => 'TEXT'.freeze,
41
69
  :class => 'TEXT'.freeze
42
70
  })
43
-
44
- module Coersion
45
71
 
46
- def type_cast_boolean(value)
47
- case value
48
- when TrueClass, FalseClass then value
49
- when "1", "true", "TRUE" then true
50
- when "0", nil then false
51
- else "Can't type-cast #{value.inspect} to a boolean"
52
- end
72
+ TABLE_QUOTING_CHARACTER = '"'.freeze
73
+ COLUMN_QUOTING_CHARACTER = '"'.freeze
74
+
75
+ def type_cast_boolean(value)
76
+ case value
77
+ when TrueClass, FalseClass then value
78
+ when "1", "true", "TRUE" then true
79
+ when "0", nil then false
80
+ else "Can't type-cast #{value.inspect} to a boolean"
53
81
  end
82
+ end
54
83
 
55
- def type_cast_datetime(value)
56
- case value
57
- when DateTime then value
58
- when Date then DateTime.new(value)
59
- when String then DateTime::parse(value)
60
- else "Can't type-cast #{value.inspect} to a datetime"
61
- end
84
+ def type_cast_datetime(value)
85
+ case value
86
+ when DateTime then value
87
+ when Date then DateTime.new(value)
88
+ when String then DateTime::parse(value)
89
+ else "Can't type-cast #{value.inspect} to a datetime"
62
90
  end
63
-
64
- end # module Coersion
65
-
66
- module Queries
67
-
68
- class Connection
69
-
70
- def initialize(database)
71
- @database = database
72
- @dbh = SQLite3::Database.new(database.database)
73
- database.log.debug("Initializing Connection for Database[#{database.name}]")
74
- super(database.log)
75
- end
76
-
77
- def execute(statement)
78
- send_query(statement)
79
- Result.new(@dbh.total_changes, @dbh.last_insert_row_id)
80
- end
81
-
82
- def query(statement)
83
- Reader.new(send_query(statement))
84
- end
85
-
86
- def close
87
- @dbh.close
88
- end
89
-
90
- private
91
- def send_query(statement)
92
- sql = statement.respond_to?(:to_sql) ? statement.to_sql : statement
93
- log.debug("Database[#{@database.name}] => #{sql}")
94
- @dbh.query(sql)
95
- end
96
-
97
- end # class Connection
91
+ end
92
+
93
+ module Commands
98
94
 
99
- class Reader
100
-
101
- include Enumerable
102
-
103
- def initialize(results)
104
- @results = results
105
- @columns = {}
106
- @results.columns.each_with_index do |name, index|
107
- @columns[name] = index
108
- end
109
- end
110
-
111
- def eof?
112
- @results.eof?
113
- end
114
-
115
- def records_affected
116
- @results.entries.size
117
- end
118
-
119
- def next
120
- @current_row = @results.next
121
- self
122
- end
123
-
124
- def each
125
- until self.next.eof?
126
- yield(self)
127
- end
128
- end
129
-
130
- def [](column)
131
- index = @columns[column]
132
- return nil if index.nil? || @current_row.nil?
133
- @current_row[index]
134
- end
135
-
136
- def each_pair
137
- @columns.each_pair do |column_name, index|
138
- yield(column_name, @current_row.nil? ? nil : @current_row[index])
139
- end
95
+ class TableExistsCommand
96
+ def to_sql
97
+ "SELECT name FROM sqlite_master WHERE type = \"table\" AND name = #{table_name}"
140
98
  end
141
99
 
142
- def close
143
- @results.close
144
- end
145
-
146
- end # class Reader
147
-
148
- class TableExistsStatement
149
- def to_sql
150
- "SELECT name FROM sqlite_master WHERE type = \"table\" AND name = #{@database.quote_value(@database[@klass].name)}"
100
+ def call
101
+ reader = @adapter.connection { |db| db.query(to_sql) }
102
+ result = reader.entries.size > 0
103
+ reader.close
104
+ result
151
105
  end
152
- end # class TableExistsStatement
106
+ end # class TableExistsCommand
153
107
 
154
- class CreateTableStatement
155
- def to_sql
156
- table = @database[@klass]
108
+ class SaveCommand
109
+ def to_create_table_sql
110
+ table = @adapter[@instance]
157
111
 
158
112
  sql = "CREATE TABLE " << table.to_sql
159
113
 
@@ -165,7 +119,7 @@ module DataMapper
165
119
  end
166
120
 
167
121
  def column_long_form(column)
168
- long_form = "#{column.to_sql} #{@database.adapter.class::TYPES[column.type] || column.type}"
122
+ long_form = "#{column.to_sql} #{@adapter.class::TYPES[column.type] || column.type}"
169
123
 
170
124
  long_form << " NOT NULL" unless column.nullable?
171
125
  long_form << " PRIMARY KEY" if column.key?
@@ -173,16 +127,101 @@ module DataMapper
173
127
 
174
128
  return long_form
175
129
  end
176
- end # class CreateTableStatement
177
-
178
- class TruncateTableStatement
179
- def to_sql
180
- "DELETE FROM " << @database[@klass].to_sql
130
+
131
+ def execute_insert(sql)
132
+ @adapter.connection do |db|
133
+ db.query(sql)
134
+ db.last_insert_row_id
135
+ end
181
136
  end
182
- end # class TruncateTableStatement
137
+
138
+ def execute_update(sql)
139
+ @adapter.connection do |db|
140
+ db.query(sql)
141
+ db.total_changes > 0
142
+ end
143
+ end
144
+
145
+ def execute_create_table(sql)
146
+ @adapter.connection { |db| db.query(sql) }
147
+ true
148
+ end
149
+ end # class SaveCommand
183
150
 
184
- end # module Queries
151
+ class DeleteCommand
152
+ def to_truncate_sql
153
+ "DELETE FROM " << @adapter[@klass_or_instance].to_sql
154
+ end
155
+
156
+ def execute(sql)
157
+ @adapter.connection do |db|
158
+ db.query(sql)
159
+ db.total_changes > 0
160
+ end
161
+ end
162
+
163
+ def execute_drop(sql)
164
+ @adapter.connection { |db| db.query(sql) }
165
+ true
166
+ end
167
+ end # class DeleteCommand
168
+
169
+ class LoadCommand
170
+ def eof?(reader)
171
+ reader.eof?
172
+ end
173
+
174
+ def close_reader(reader)
175
+ reader.close
176
+ end
177
+
178
+ def execute(sql)
179
+ @adapter.connection { |db| db.query(to_sql) }
180
+ end
181
+
182
+ def fetch_one(reader)
183
+ load(reader.next)
184
+ end
185
+
186
+ def fetch_all(reader)
187
+ fields = nil
188
+ rows = []
189
+
190
+ until reader.eof?
191
+ hash = reader.next
192
+ break if hash.nil?
193
+
194
+ fields = hash.keys.select { |field| field.is_a?(String) } unless fields
195
+
196
+ rows << fields.map { |name| hash[name] }
197
+ end
198
+
199
+ load_instances(fields, rows)
200
+ end
201
+
202
+ def fetch_structs(reader)
203
+ fields = nil
204
+ rows = []
205
+
206
+ until reader.eof?
207
+ hash = reader.next
208
+ break if hash.nil?
209
+
210
+ fields = hash.keys.select { |field| field.is_a?(String) } unless fields
211
+
212
+ rows << fields.map { |name| hash[name] }
213
+ end
214
+
215
+ fields = fields.inject({}) do |h,f|
216
+ h[f] = fields.index(f); h
217
+ end
218
+
219
+ load_structs(fields, rows)
220
+ end
221
+ end
185
222
 
223
+ end # module Commands
224
+
186
225
  end # class Sqlite3Adapter
187
226
 
188
227
  end # module Adapters