datamapper 0.1.0 → 0.1.1

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 (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