datamapper 0.1.1 → 0.2.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 (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