datamapper 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. data/CHANGELOG +65 -0
  2. data/README +193 -1
  3. data/do_performance.rb +153 -0
  4. data/environment.rb +45 -0
  5. data/example.rb +119 -22
  6. data/lib/data_mapper.rb +36 -16
  7. data/lib/data_mapper/adapters/abstract_adapter.rb +8 -0
  8. data/lib/data_mapper/adapters/data_object_adapter.rb +360 -0
  9. data/lib/data_mapper/adapters/mysql_adapter.rb +30 -179
  10. data/lib/data_mapper/adapters/postgresql_adapter.rb +90 -199
  11. data/lib/data_mapper/adapters/sql/coersion.rb +32 -3
  12. data/lib/data_mapper/adapters/sql/commands/conditions.rb +97 -128
  13. data/lib/data_mapper/adapters/sql/commands/load_command.rb +234 -231
  14. data/lib/data_mapper/adapters/sql/commands/loader.rb +99 -0
  15. data/lib/data_mapper/adapters/sql/mappings/associations_set.rb +30 -0
  16. data/lib/data_mapper/adapters/sql/mappings/column.rb +68 -6
  17. data/lib/data_mapper/adapters/sql/mappings/schema.rb +6 -3
  18. data/lib/data_mapper/adapters/sql/mappings/table.rb +71 -42
  19. data/lib/data_mapper/adapters/sql/quoting.rb +8 -2
  20. data/lib/data_mapper/adapters/sqlite3_adapter.rb +32 -201
  21. data/lib/data_mapper/associations.rb +21 -7
  22. data/lib/data_mapper/associations/belongs_to_association.rb +96 -80
  23. data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +158 -67
  24. data/lib/data_mapper/associations/has_many_association.rb +96 -78
  25. data/lib/data_mapper/associations/has_n_association.rb +64 -0
  26. data/lib/data_mapper/associations/has_one_association.rb +49 -79
  27. data/lib/data_mapper/associations/reference.rb +47 -0
  28. data/lib/data_mapper/base.rb +216 -50
  29. data/lib/data_mapper/callbacks.rb +71 -24
  30. data/lib/data_mapper/{session.rb → context.rb} +20 -8
  31. data/lib/data_mapper/database.rb +176 -45
  32. data/lib/data_mapper/embedded_value.rb +65 -0
  33. data/lib/data_mapper/identity_map.rb +12 -4
  34. data/lib/data_mapper/support/active_record_impersonation.rb +12 -8
  35. data/lib/data_mapper/support/enumerable.rb +8 -0
  36. data/lib/data_mapper/support/serialization.rb +13 -0
  37. data/lib/data_mapper/support/string.rb +1 -12
  38. data/lib/data_mapper/support/symbol.rb +3 -0
  39. data/lib/data_mapper/validations/unique_validator.rb +1 -2
  40. data/lib/data_mapper/validations/validation_helper.rb +18 -1
  41. data/performance.rb +109 -34
  42. data/plugins/can_has_sphinx/LICENSE +23 -0
  43. data/plugins/can_has_sphinx/README +4 -0
  44. data/plugins/can_has_sphinx/REVISION +1 -0
  45. data/plugins/can_has_sphinx/Rakefile +22 -0
  46. data/plugins/can_has_sphinx/init.rb +1 -0
  47. data/plugins/can_has_sphinx/install.rb +1 -0
  48. data/plugins/can_has_sphinx/lib/acts_as_sphinx.rb +123 -0
  49. data/plugins/can_has_sphinx/lib/sphinx.rb +460 -0
  50. data/plugins/can_has_sphinx/scripts/sphinx.sh +47 -0
  51. data/plugins/can_has_sphinx/tasks/acts_as_sphinx_tasks.rake +41 -0
  52. data/plugins/dataobjects/REVISION +1 -0
  53. data/plugins/dataobjects/Rakefile +7 -0
  54. data/plugins/dataobjects/do.rb +246 -0
  55. data/plugins/dataobjects/do_mysql.rb +179 -0
  56. data/plugins/dataobjects/do_postgres.rb +181 -0
  57. data/plugins/dataobjects/do_sqlite3.rb +153 -0
  58. data/plugins/dataobjects/spec/do_spec.rb +150 -0
  59. data/plugins/dataobjects/spec/spec_helper.rb +81 -0
  60. data/plugins/dataobjects/swig_mysql/do_mysql.bundle +0 -0
  61. data/plugins/dataobjects/swig_mysql/extconf.rb +33 -0
  62. data/plugins/dataobjects/swig_mysql/mysql_c.c +18800 -0
  63. data/plugins/dataobjects/swig_mysql/mysql_c.i +8 -0
  64. data/plugins/dataobjects/swig_mysql/mysql_supp.i +46 -0
  65. data/plugins/dataobjects/swig_postgres/Makefile +146 -0
  66. data/plugins/dataobjects/swig_postgres/extconf.rb +29 -0
  67. data/plugins/dataobjects/swig_postgres/postgres_c.bundle +0 -0
  68. data/plugins/dataobjects/swig_postgres/postgres_c.c +8185 -0
  69. data/plugins/dataobjects/swig_postgres/postgres_c.i +73 -0
  70. data/plugins/dataobjects/swig_sqlite/db +0 -0
  71. data/plugins/dataobjects/swig_sqlite/extconf.rb +9 -0
  72. data/plugins/dataobjects/swig_sqlite/sqlite3_c.c +4725 -0
  73. data/plugins/dataobjects/swig_sqlite/sqlite_c.i +168 -0
  74. data/rakefile.rb +45 -23
  75. data/spec/acts_as_tree_spec.rb +39 -0
  76. data/spec/associations_spec.rb +220 -0
  77. data/spec/attributes_spec.rb +15 -0
  78. data/spec/base_spec.rb +44 -0
  79. data/spec/callbacks_spec.rb +45 -0
  80. data/spec/can_has_sphinx.rb +6 -0
  81. data/spec/coersion_spec.rb +34 -0
  82. data/spec/conditions_spec.rb +49 -0
  83. data/spec/conversions_to_yaml_spec.rb +17 -0
  84. data/spec/count_command_spec.rb +11 -0
  85. data/spec/delete_command_spec.rb +1 -1
  86. data/spec/embedded_value_spec.rb +23 -0
  87. data/spec/fixtures/animals_exhibits.yaml +2 -0
  88. data/spec/fixtures/people.yaml +18 -1
  89. data/spec/{legacy.rb → legacy_spec.rb} +3 -3
  90. data/spec/load_command_spec.rb +157 -20
  91. data/spec/magic_columns_spec.rb +9 -0
  92. data/spec/mock_adapter.rb +20 -0
  93. data/spec/models/animal.rb +1 -1
  94. data/spec/models/animals_exhibit.rb +6 -0
  95. data/spec/models/exhibit.rb +2 -0
  96. data/spec/models/person.rb +26 -1
  97. data/spec/models/project.rb +19 -0
  98. data/spec/models/sales_person.rb +1 -0
  99. data/spec/models/section.rb +6 -0
  100. data/spec/models/zoo.rb +3 -1
  101. data/spec/query_spec.rb +9 -0
  102. data/spec/save_command_spec.rb +65 -1
  103. data/spec/schema_spec.rb +89 -0
  104. data/spec/single_table_inheritance_spec.rb +27 -0
  105. data/spec/spec_helper.rb +9 -55
  106. data/spec/{symbolic_operators.rb → symbolic_operators_spec.rb} +9 -5
  107. data/spec/{validates_confirmation_of.rb → validates_confirmation_of_spec.rb} +4 -3
  108. data/spec/{validates_format_of.rb → validates_format_of_spec.rb} +5 -4
  109. data/spec/{validates_length_of.rb → validates_length_of_spec.rb} +8 -7
  110. data/spec/{validates_uniqueness_of.rb → validates_uniqueness_of_spec.rb} +7 -10
  111. data/spec/{validations.rb → validations_spec.rb} +24 -6
  112. data/tasks/drivers.rb +20 -0
  113. data/tasks/fixtures.rb +42 -0
  114. metadata +181 -42
  115. data/lib/data_mapper/adapters/sql/commands/advanced_load_command.rb +0 -140
  116. data/lib/data_mapper/adapters/sql/commands/delete_command.rb +0 -113
  117. data/lib/data_mapper/adapters/sql/commands/save_command.rb +0 -141
  118. data/lib/data_mapper/adapters/sql/commands/table_exists_command.rb +0 -33
  119. data/lib/data_mapper/adapters/sql_adapter.rb +0 -163
  120. data/lib/data_mapper/associations/advanced_has_many_association.rb +0 -55
  121. data/lib/data_mapper/support/blank_slate.rb +0 -3
  122. data/lib/data_mapper/support/proc.rb +0 -69
  123. data/lib/data_mapper/support/struct.rb +0 -26
  124. data/lib/data_mapper/unit_of_work.rb +0 -38
  125. data/spec/basic_finder.rb +0 -67
  126. data/spec/belongs_to.rb +0 -47
  127. data/spec/has_and_belongs_to_many.rb +0 -25
  128. data/spec/has_many.rb +0 -34
  129. data/spec/new_record.rb +0 -24
  130. data/spec/sub_select.rb +0 -16
  131. data/spec/support/string_spec.rb +0 -7
data/example.rb CHANGED
@@ -1,33 +1,130 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'lib/data_mapper'
3
+ ENV['LOG_NAME'] = 'example'
4
+ require 'environment'
4
5
 
5
- ENV['ADAPTER'] ||= 'mysql'
6
-
7
- DataMapper::Database.setup do
8
- adapter ENV['ADAPTER']
9
-
10
- unless ENV['LOGGER'] == 'false'
11
- log_stream 'example.log'
12
- log_level Logger::DEBUG
13
- end
6
+ # Define a fixtures helper method to load up our test data.
7
+ def fixtures(name, force = false)
8
+ entry = YAML::load_file(File.dirname(__FILE__) + "/spec/fixtures/#{name}.yaml")
9
+ klass = Kernel::const_get(Inflector.classify(Inflector.singularize(name)))
14
10
 
15
- database_name = 'data_mapper_1'
11
+ klass.auto_migrate!
16
12
 
17
- case ENV['ADAPTER']
18
- when 'postgresql' then
19
- username 'postgres'
20
- when 'mysql' then
21
- username 'root'
22
- when 'sqlite3' then
23
- database_name << '.db'
13
+ (entry.kind_of?(Array) ? entry : [entry]).each do |hash|
14
+ if hash['type']
15
+ Object::const_get(hash['type'])::create(hash)
16
+ else
17
+ klass::create(hash)
18
+ end
24
19
  end
20
+ end
21
+
22
+ # Pre-fill the database so non-destructive tests don't need to reload fixtures.
23
+ Dir[File.dirname(__FILE__) + "/spec/fixtures/*.yaml"].each do |path|
24
+ fixtures(File::basename(path).sub(/\.yaml$/, ''), true)
25
+ end
26
+
27
+ require 'irb'
28
+
29
+ database { IRB::start }
30
+
31
+ if false
32
+
33
+ # Simple example to setup a database:
34
+ DataMapper::Database.setup({
35
+ :adapter => 'mysql',
36
+ :database => 'data_mapper_1',
37
+ :username => 'root'
38
+ })
39
+
40
+ class Animal < DataMapper::Base
41
+ set_table_name 'animals' # Just as an example. Same inflector as Rails,
42
+ # so this really isn't necessary.
43
+
44
+ property :name, :string
45
+ property :notes, :string, :lazy => true
25
46
 
26
- database database_name
47
+ has_and_belongs_to_many :exhibits
48
+ end
49
+
50
+ class Exhibit < DataMapper::Base
51
+ property :name, :string
52
+ belongs_to :zoo
53
+ end
54
+
55
+ class Zoo < DataMapper::Base
56
+ property :name, :string
57
+ has_many :exhibits
27
58
  end
28
59
 
29
- Dir[File.dirname(__FILE__) + '/spec/models/*.rb'].each do |path|
30
- load path
60
+ class Person < DataMapper::Base
61
+
62
+ property :name, :string
63
+ property :age, :integer
64
+ property :occupation, :string
65
+ property :notes, :text, :lazy => true
66
+
67
+ # Generates Person::Address class:
68
+ embed :address do
69
+ property :street, :string
70
+ property :city, :string
71
+ property :state, :string, :size => 2
72
+ property :postal_code, :string
73
+ end
31
74
  end
32
75
 
33
- # p Zoo.all
76
+ # Compatible with ActiveRecord finder syntax:
77
+ Zoo.find(1)
78
+ Zoo.find(:first, :conditions => ['name = ?', 'Galveston'])
79
+ Zoo.find(:all)
80
+
81
+ # These are options as well:
82
+ Zoo[1]
83
+ Zoo.first(:name => 'Galveston')
84
+ Zoo.all
85
+
86
+ # Or even this as an alias to ::first:
87
+ Zoo[:name => 'Galveston']
88
+
89
+ # EmbeddedValues are just nice sugar to partition
90
+ # denormalized data.
91
+ Person.first.address.city
92
+
93
+ # Remove all data in a table...
94
+ Person.truncate!
95
+
96
+ # Create a new object...
97
+ Person::create(:name => 'Sam', :age => 30, :occupation => 'Software Monkey')
98
+
99
+ # Saving only updates the values that have changed,
100
+ # and is skipped entirely if the object is not dirty.
101
+ dumbo = Animal.first(:name => 'Elephant')
102
+ dumbo.notes = 'He can fly!'
103
+ dumbo.save # returns true
104
+ dumbo.save # The object is no longer dirty, so returns false
105
+
106
+ # DataMapper associations are loaded as sets.
107
+ # Here's the code:
108
+ Zoo.all.each { |zoo| zoo.exhibits.entries }
109
+ # The important bit to understand about the above is that
110
+ # every Zoo that was loaded by Zoo.all has a reference to
111
+ # every other Zoo it was loaded with through Zoo#loaded_set.
112
+ # This is then used to load all other instances in the set
113
+ # when the association of one instance is accessed. So while
114
+ # it looks like we'd run into the dreaded 1+N query problem
115
+ # with the above, we actually avoid it entirely. The above
116
+ # code will only execute two queries. The first to find all
117
+ # zoos, the second to load all exhibits with zoo_id's that
118
+ # are a part of the set of loaded zoos.
119
+
120
+
121
+ # Objects within the same session are uniqued, so this is both
122
+ # faster, and fulfills obvious expectations.
123
+ database do
124
+ Zoo.first == Zoo.first
125
+ end
126
+
127
+ # DataMapper find_by_sql equivilent
128
+ database.query("SELECT * FROM zoos")
129
+
130
+ end
data/lib/data_mapper.rb CHANGED
@@ -1,35 +1,55 @@
1
+ # This file begins the loading sequence.
2
+ #
3
+ # Quick Overview:
4
+ # * Requires set, fastthread, support libs, and base
5
+ # * Sets the applications root and environment for compatibility with rails or merb
6
+ # * Checks for the database.yml and loads it if it exists
7
+ # * Sets up the database using the config from the yaml file or from the environment
8
+ # *
9
+
1
10
  # This line just let's us require anything in the +lib+ sub-folder
2
11
  # without specifying a full path.
12
+
3
13
  $LOAD_PATH.unshift(File.dirname(__FILE__))
4
14
 
15
+ DM_PLUGINS_ROOT = (File.dirname(__FILE__) + '/../plugins')
16
+
5
17
  # Require the basics...
18
+ require 'yaml'
6
19
  require 'set'
7
20
  require 'fastthread'
8
21
  require 'data_mapper/support/blank'
9
22
  require 'data_mapper/support/enumerable'
10
23
  require 'data_mapper/support/symbol'
11
24
  require 'data_mapper/support/string'
12
- require 'data_mapper/support/proc'
13
25
  require 'data_mapper/support/inflector'
14
- require 'data_mapper/support/struct'
15
26
  require 'data_mapper/database'
16
27
  require 'data_mapper/base'
17
28
 
18
- # This block of code is for compatibility with Ruby On Rails' database.yml
29
+ # This block of code is for compatibility with Ruby On Rails' or Merb's database.yml
19
30
  # file, allowing you to simply require the data_mapper.rb in your
20
31
  # Rails application's environment.rb to configure the DataMapper.
21
- if defined? RAILS_ENV
22
- require 'yaml'
32
+
33
+ application_root, application_environment = *if defined?(MERB_ROOT)
34
+ [MERB_ROOT, MERB_ENV]
35
+ elsif defined?(RAILS_ROOT)
36
+ [RAILS_ROOT, RAILS_ENV]
37
+ end
38
+
39
+ DM_APP_ROOT = application_root || Dir::pwd
40
+
41
+ if application_root && File.exists?(application_root + '/config/database.yml')
42
+
43
+ database_configurations = YAML::load_file(application_root + '/config/database.yml')
44
+ current_database_config = database_configurations[application_environment] || database_configurations[application_environment.to_sym]
45
+
46
+ default_database_config = {
47
+ :adapter => current_database_config['adapter'] || current_database_config[:adapter],
48
+ :host => current_database_config['host'] || current_database_config[:host],
49
+ :database => current_database_config['database'] || current_database_config[:database],
50
+ :username => current_database_config['username'] || current_database_config[:username],
51
+ :password => current_database_config['password'] || current_database_config[:password]
52
+ }
23
53
 
24
- rails_config = YAML::load_file(RAILS_ROOT + '/config/database.yml')
25
- current_config = rails_config[RAILS_ENV.to_s]
26
-
27
- DataMapper::Database.setup do
28
- adapter current_config['adapter']
29
- host current_config['host']
30
- database current_config['database']
31
- username current_config['username']
32
- password current_config['password']
33
- cache WeakHash::Factory
34
- end
54
+ DataMapper::Database.setup(default_database_config)
35
55
  end
@@ -9,6 +9,14 @@ module DataMapper
9
9
  @configuration = configuration
10
10
  end
11
11
 
12
+ def index_path
13
+ @configuration.index_path
14
+ end
15
+
16
+ def name
17
+ @configuration.name
18
+ end
19
+
12
20
  def delete(instance_or_klass, options = nil)
13
21
  raise NotImplementedError.new
14
22
  end
@@ -0,0 +1,360 @@
1
+ require 'data_mapper/adapters/abstract_adapter'
2
+ require 'data_mapper/adapters/sql/commands/load_command'
3
+ require 'data_mapper/adapters/sql/coersion'
4
+ require 'data_mapper/adapters/sql/quoting'
5
+ require 'data_mapper/adapters/sql/mappings/schema'
6
+ require 'data_mapper/support/connection_pool'
7
+
8
+ module DataMapper
9
+
10
+ # An Adapter is really a Factory for three types of object,
11
+ # so they can be selectively sub-classed where needed.
12
+ #
13
+ # The first type is a Query. The Query is an object describing
14
+ # the database-specific operations we wish to perform, in an
15
+ # abstract manner. For example: While most if not all databases
16
+ # support a mechanism for limiting the size of results returned,
17
+ # some use a "LIMIT" keyword, while others use a "TOP" keyword.
18
+ # We can set a SelectStatement#limit field then, and allow
19
+ # the adapter to override the underlying SQL generated.
20
+ # Refer to DataMapper::Queries.
21
+ #
22
+ # The final type provided is a DataMapper::Transaction.
23
+ # Transactions are duck-typed Connections that span multiple queries.
24
+ #
25
+ # Note: It is assumed that the Adapter implements it's own
26
+ # ConnectionPool if any since some libraries implement their own at
27
+ # a low-level, and it wouldn't make sense to pay a performance
28
+ # cost twice by implementing a secondary pool in the DataMapper itself.
29
+ # If the library being adapted does not provide such functionality,
30
+ # DataMapper::Support::ConnectionPool can be used.
31
+ module Adapters
32
+
33
+ # You must inherit from the DoAdapter, and implement the
34
+ # required methods to adapt a database library for use with the DataMapper.
35
+ #
36
+ # NOTE: By inheriting from DoAdapter, you get a copy of all the
37
+ # standard sub-modules (Quoting, Coersion and Queries) in your own Adapter.
38
+ # You can extend and overwrite these copies without affecting the originals.
39
+ class DataObjectAdapter < AbstractAdapter
40
+
41
+ $LOAD_PATH << (DM_PLUGINS_ROOT + '/dataobjects')
42
+
43
+ FIND_OPTIONS = [
44
+ :select, :offset, :limit, :class, :include, :shallow_include, :reload, :conditions, :order, :intercept_load
45
+ ]
46
+
47
+ TABLE_QUOTING_CHARACTER = '`'.freeze
48
+ COLUMN_QUOTING_CHARACTER = '`'.freeze
49
+
50
+ def initialize(configuration)
51
+ super
52
+
53
+ unless @configuration.single_threaded?
54
+ @connection_pool = Support::ConnectionPool.new { create_connection }
55
+ end
56
+ end
57
+
58
+ def create_connection
59
+ raise NotImplementedError.new
60
+ end
61
+
62
+ # Yields an available connection. Flushes the connection-pool and reconnects
63
+ # if the connection returns an error.
64
+ def connection
65
+ begin
66
+ # Yield the appropriate connection
67
+ if @configuration.single_threaded?
68
+ yield(@active_connection || @active_connection = create_connection)
69
+ else
70
+ @connection_pool.hold { |active_connection| yield(active_connection) }
71
+ end
72
+ rescue => execution_error
73
+ # Log error on failure
74
+ @configuration.log.error(execution_error)
75
+
76
+ # Close all open connections, assuming that if one
77
+ # had an error, it's likely due to a lost connection,
78
+ # in which case all connections are likely broken.
79
+ begin
80
+ if @configuration.single_threaded?
81
+ @active_connection.close
82
+ else
83
+ @connection_pool.available_connections.each do |active_connection|
84
+ active_connection.close
85
+ end
86
+ end
87
+ rescue => close_connection_error
88
+ # An error on closing the connection is almost expected
89
+ # if the socket is broken.
90
+ @configuration.log.warn(close_connection_error)
91
+ end
92
+
93
+ # Reopen fresh connections.
94
+ if @configuration.single_threaded?
95
+ @active_connection = create_connection
96
+ else
97
+ @connection_pool.available_connections.clear
98
+ end
99
+
100
+ raise execution_error
101
+ end
102
+ end
103
+
104
+ def transaction(&block)
105
+ raise NotImplementedError.new
106
+ end
107
+
108
+ def execute(*args)
109
+ connection do |db|
110
+ sql = escape_sql(*args)
111
+ log.debug { sql }
112
+ result = nil
113
+
114
+ command = db.create_command(sql)
115
+
116
+ if block_given?
117
+ reader = command.execute_reader
118
+ result = yield(reader)
119
+ reader.close
120
+ else
121
+ result = command.execute_non_query
122
+ end
123
+
124
+ result
125
+ end
126
+ rescue => e
127
+ handle_error(e)
128
+ end
129
+
130
+ def query(*args)
131
+ execute(*args) do |reader|
132
+ fields = reader.fields.map { |field| Inflector.underscore(field).to_sym }
133
+
134
+ results = []
135
+
136
+ if fields.size > 1
137
+ struct = Struct.new(*fields)
138
+
139
+ reader.each do
140
+ results << struct.new(*reader.current_row)
141
+ end
142
+ else
143
+ reader.each do
144
+ results << reader.item(0)
145
+ end
146
+ end
147
+
148
+ results
149
+ end
150
+ end
151
+
152
+ def handle_error(error)
153
+ raise error
154
+ end
155
+
156
+ def schema
157
+ @schema || ( @schema = Mappings::Schema.new(self, @configuration.database) )
158
+ end
159
+
160
+ def table_exists?(name)
161
+ execute(table(name).to_exists_sql) { |reader| reader.has_rows? }
162
+ end
163
+
164
+ def truncate(session, name)
165
+ result = execute("TRUNCATE TABLE #{table(name).to_sql}")
166
+ session.identity_map.clear!(name)
167
+ result.to_i > 0
168
+ end
169
+
170
+ def drop(session, name)
171
+ result = execute("DROP TABLE #{table(name).to_sql}")
172
+ session.identity_map.clear!(name)
173
+ true
174
+ end
175
+
176
+ def create_table(name)
177
+ execute(table(name).to_create_table_sql); true
178
+ end
179
+
180
+ def delete(session, instance)
181
+ table = self.table(instance)
182
+
183
+ if instance.is_a?(Class)
184
+ execute("DELETE FROM #{table.to_sql}")
185
+ session.identity_map.clear!(instance)
186
+ else
187
+ callback(instance, :before_destroy)
188
+
189
+ if execute("DELETE FROM #{table.to_sql} WHERE #{table.key.to_sql} = #{quote_value(instance.key)}").to_i > 0
190
+ instance.instance_variable_set(:@new_record, true)
191
+ instance.session = session
192
+ instance.original_hashes.clear
193
+ session.identity_map.delete(instance)
194
+ callback(instance, :after_destroy)
195
+ end
196
+ end
197
+ end
198
+
199
+ def save(session, instance)
200
+ case instance
201
+ when Class, Mappings::Table then create_table(instance)
202
+ when DataMapper::Base then
203
+ return false unless instance.dirty? && instance.valid?
204
+
205
+ callback(instance, :before_save)
206
+
207
+ table = self.table(instance)
208
+ attributes = instance.dirty_attributes
209
+
210
+ unless attributes.empty?
211
+ attributes[:type] = instance.class.name if table.multi_class?
212
+
213
+ # INSERT
214
+ result = if instance.new_record?
215
+ callback(instance, :before_create)
216
+
217
+ keys = []
218
+ values = []
219
+ attributes.each_pair do |key, value|
220
+ keys << table[key].to_sql
221
+ values << value
222
+ end
223
+
224
+ # Formatting is a bit off here, but it looks nicer in the log this way.
225
+ insert_id = execute("INSERT INTO #{table.to_sql} (#{keys.join(', ')}) VALUES (#{values.map { |v| quote_value(v) }.join(', ')})").last_insert_row
226
+ instance.instance_variable_set(:@new_record, false)
227
+ instance.key = insert_id if table.key.serial? && !attributes.include?(table.key.name)
228
+ session.identity_map.set(instance)
229
+ callback(instance, :after_create)
230
+ # UPDATE
231
+ else
232
+ callback(instance, :before_update)
233
+
234
+ sql = "UPDATE " << table.to_sql << " SET "
235
+
236
+ sql << attributes.map do |key, value|
237
+ "#{table[key].to_sql} = #{quote_value(value)}"
238
+ end.join(', ')
239
+
240
+ sql << " WHERE #{table.key.to_sql} = " << quote_value(instance.key)
241
+
242
+ execute(sql).to_i > 0 && callback(instance, :after_update)
243
+ end
244
+ end # unless attributes.empty?
245
+
246
+ instance.attributes.each_pair do |name, value|
247
+ instance.original_hashes[name] = value.hash
248
+ end
249
+
250
+ instance.loaded_associations.each do |association|
251
+ association.save if association.respond_to?(:save)
252
+ end
253
+
254
+ instance.session = session
255
+ callback(instance, :after_save)
256
+ result
257
+ end
258
+ rescue => error
259
+ log.error(error)
260
+ raise error
261
+ end
262
+
263
+ def load(session, klass, options)
264
+ self.class::Commands::LoadCommand.new(self, session, klass, options).call
265
+ end
266
+
267
+ def count(klass_or_instance, options)
268
+ query("SELECT COUNT(*) AS row_count FROM " + table(klass_or_instance).to_sql).first.to_i
269
+ end
270
+
271
+ def table(instance)
272
+ case instance
273
+ when DataMapper::Adapters::Sql::Mappings::Table then instance
274
+ when DataMapper::Base then schema[instance.class]
275
+ when Class, String then schema[instance]
276
+ else raise "Don't know how to map #{instance.inspect} to a table."
277
+ end
278
+ end
279
+
280
+ def callback(instance, callback_name)
281
+ instance.class.callbacks.execute(callback_name, instance)
282
+ end
283
+
284
+ # Escape a string of SQL with a set of arguments.
285
+ # The first argument is assumed to be the SQL to escape,
286
+ # the remaining arguments (if any) are assumed to be
287
+ # values to escape and interpolate.
288
+ #
289
+ # ==== Examples
290
+ # escape_sql("SELECT * FROM zoos")
291
+ # # => "SELECT * FROM zoos"
292
+ #
293
+ # escape_sql("SELECT * FROM zoos WHERE name = ?", "Dallas")
294
+ # # => "SELECT * FROM zoos WHERE name = `Dallas`"
295
+ #
296
+ # escape_sql("SELECT * FROM zoos WHERE name = ? AND acreage > ?", "Dallas", 40)
297
+ # # => "SELECT * FROM zoos WHERE name = `Dallas` AND acreage > 40"
298
+ #
299
+ # ==== Warning
300
+ # This method is meant mostly for adapters that don't support
301
+ # bind-parameters.
302
+ def escape_sql(*args)
303
+ sql = args.shift
304
+
305
+ unless args.empty?
306
+ sql.gsub!(/\?/) do |x|
307
+ quote_value(args.shift)
308
+ end
309
+ end
310
+
311
+ sql
312
+ end
313
+
314
+ # This callback copies and sub-classes modules and classes
315
+ # in the DoAdapter to the inherited class so you don't
316
+ # have to copy and paste large blocks of code from the
317
+ # DoAdapter.
318
+ #
319
+ # Basically, when inheriting from the DoAdapter, you
320
+ # aren't just inheriting a single class, you're inheriting
321
+ # a whole graph of Types. For convenience.
322
+ def self.inherited(base)
323
+
324
+ commands = base.const_set('Commands', Module.new)
325
+
326
+ Sql::Commands.constants.each do |name|
327
+ commands.const_set(name, Class.new(Sql::Commands.const_get(name)))
328
+ end
329
+
330
+ mappings = base.const_set('Mappings', Module.new)
331
+
332
+ Sql::Mappings.constants.each do |name|
333
+ mappings.const_set(name, Class.new(Sql::Mappings.const_get(name)))
334
+ end
335
+
336
+ base.const_set('TYPES', TYPES.dup)
337
+ base.const_set('FIND_OPTIONS', FIND_OPTIONS.dup)
338
+
339
+ super
340
+ end
341
+
342
+ TYPES = {
343
+ :integer => 'int'.freeze,
344
+ :string => 'varchar'.freeze,
345
+ :text => 'text'.freeze,
346
+ :class => 'varchar'.freeze,
347
+ :decimal => 'decimal'.freeze,
348
+ :float => 'float'.freeze,
349
+ :datetime => 'datetime'.freeze,
350
+ :date => 'date'.freeze
351
+ }
352
+
353
+ include Sql
354
+ include Quoting
355
+ include Coersion
356
+
357
+ end # class DoAdapter
358
+
359
+ end # module Adapters
360
+ end # module DataMapper