datamapper 0.1.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 (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,2 @@
1
+ -- 0.1.0
2
+ * Initial Public Release
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2007 Samuel Smoot
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1 @@
1
+ Start out with example.rb and go from there if you want to try it out live.
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH.unshift('lib')
2
+
3
+ require 'data_mapper'
4
+
5
+ if ENV['ADAPTER'] == 'sqlite3'
6
+ DataMapper::Database.setup do
7
+ adapter 'sqlite3'
8
+ database 'data_mapper_1.db'
9
+ log_stream 'example.log'
10
+ log_level Logger::DEBUG
11
+ end
12
+ else
13
+ DataMapper::Database.setup do
14
+ adapter 'mysql'
15
+ host 'localhost'
16
+ username 'root'
17
+ database 'data_mapper_1'
18
+ log_stream 'example.log'
19
+ log_level Logger::DEBUG
20
+ end
21
+ end
22
+
23
+ Dir[File.dirname(__FILE__) + '/spec/models/*.rb'].each do |path|
24
+ load path
25
+ end
@@ -0,0 +1,30 @@
1
+ # This line just let's us require anything in the +lib+ sub-folder
2
+ # without specifying a full path.
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
4
+
5
+ # Require the basics...
6
+ require 'data_mapper/support/symbol'
7
+ require 'data_mapper/support/string'
8
+ require 'data_mapper/support/proc'
9
+ require 'data_mapper/support/inflector'
10
+ require 'data_mapper/database'
11
+ require 'data_mapper/base'
12
+
13
+ # This block of code is for compatibility with Ruby On Rails' database.yml
14
+ # file, allowing you to simply require the data_mapper.rb in your
15
+ # Rails application's environment.rb to configure the DataMapper.
16
+ if defined? RAILS_ENV
17
+ require 'yaml'
18
+
19
+ rails_config = YAML::load_file(RAILS_ROOT + '/config/database.yml')
20
+ current_config = rails_config[RAILS_ENV.to_s]
21
+
22
+ DataMapper::Database.setup do
23
+ adapter current_config['adapter']
24
+ host current_config['host']
25
+ database current_config['database']
26
+ username current_config['username']
27
+ password current_config['password']
28
+ cache WeakHash::Factory
29
+ end
30
+ end
@@ -0,0 +1,229 @@
1
+ require 'data_mapper/queries/select_statement'
2
+ require 'data_mapper/queries/insert_statement'
3
+ require 'data_mapper/queries/update_statement'
4
+ require 'data_mapper/queries/delete_statement'
5
+ require 'data_mapper/queries/truncate_table_statement'
6
+ require 'data_mapper/queries/create_table_statement'
7
+ require 'data_mapper/queries/drop_table_statement'
8
+ require 'data_mapper/queries/table_exists_statement'
9
+ require 'data_mapper/queries/connection'
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 Abstract::Adapter, and implement the
41
+ # required methods to adapt a database library for use with the DataMapper.
42
+ #
43
+ # NOTE: By inheriting from AbstractAdapter, 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 AbstractAdapter
47
+
48
+ # Instantiate an Adapter by passing it a DataMapper::Database
49
+ # object for configuration.
50
+ def initialize(configuration)
51
+ @configuration = configuration
52
+ end
53
+
54
+ def connection(&block)
55
+ raise NotImplementedError.new
56
+ end
57
+
58
+ def transaction(&block)
59
+ raise NotImplementedError.new
60
+ end
61
+
62
+ # This callback copies and sub-classes modules and classes
63
+ # in the AbstractAdapter to the inherited class so you don't
64
+ # have to copy and paste large blocks of code from the
65
+ # AbstractAdapter.
66
+ #
67
+ # Basically, when inheriting from the AbstractAdapter, you
68
+ # aren't just inheriting a single class, you're inheriting
69
+ # a whole graph of Types. For convenience.
70
+ def self.inherited(base)
71
+
72
+ quoting = base.const_set('Quoting', Module.new)
73
+ quoting.send(:include, Quoting)
74
+
75
+ coersion = base.const_set('Coersion', Module.new)
76
+ coersion.send(:include, Coersion)
77
+
78
+ queries = base.const_set('Queries', Module.new)
79
+
80
+ Queries.constants.each do |name|
81
+ queries.const_set(name, Class.new(Queries.const_get(name)))
82
+ end
83
+
84
+ base.const_set('TYPES', TYPES.dup)
85
+
86
+ base.const_set('SYNTAX', SYNTAX.dup)
87
+ end
88
+
89
+ TYPES = {
90
+ :integer => 'int'.freeze,
91
+ :string => 'varchar'.freeze,
92
+ :text => 'text'.freeze,
93
+ :class => 'varchar'.freeze
94
+ }
95
+
96
+ SYNTAX = {
97
+ :auto_increment => 'auto_increment'.freeze
98
+ }
99
+
100
+ # Quoting is a mixin that extends your DataMapper::Database singleton-class
101
+ # to allow for object-name and value quoting to be exposed to the queries.
102
+ #
103
+ # DESIGN: Is there any need for this outside of the query objects? Should
104
+ # we just include it in our query object subclasses and not rely on a Quoting
105
+ # mixin being part of the "standard" Adapter interface?
106
+ module Quoting
107
+
108
+ def quote_table_name(name)
109
+ name.ensure_wrapped_with('"')
110
+ end
111
+
112
+ def quote_column_name(name)
113
+ name.ensure_wrapped_with('"')
114
+ end
115
+
116
+ def quote_value(value)
117
+ return 'NULL' if value.nil?
118
+
119
+ case value
120
+ when Numeric then value.to_s
121
+ when String then "'#{value.gsub("'", "''")}'"
122
+ when Class then "'#{value.name}'"
123
+ when Date then "'#{value.to_s}'"
124
+ when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
125
+ when TrueClass, FalseClass then value.to_s.upcase
126
+ else raise "Don't know how to quote #{value.inspect}"
127
+ end
128
+ end
129
+
130
+ end # module Quoting
131
+
132
+ # Coersion is a mixin that allows for coercing database values to Ruby Types.
133
+ #
134
+ # DESIGN: Probably should handle the opposite scenario here too. I believe that's
135
+ # currently in DataMapper::Database, which is obviously not a very good spot for
136
+ # it.
137
+ module Coersion
138
+
139
+ def type_cast_value(type, raw_value)
140
+ return nil if raw_value.nil?
141
+
142
+ case type
143
+ when :class then Kernel::const_get(raw_value)
144
+ when :string, :text then
145
+ return nil if raw_value.nil?
146
+ value_as_string = raw_value.to_s.strip
147
+ return nil if value_as_string.empty?
148
+ value_as_string
149
+ when :integer then
150
+ return nil if raw_value.nil? || (raw_value.kind_of?(String) && raw_value.empty?)
151
+ begin
152
+ Integer(raw_value)
153
+ rescue ArgumentError
154
+ nil
155
+ end
156
+ else
157
+ if respond_to?("type_cast_#{type}")
158
+ send("type_cast_#{type}", raw_value)
159
+ else
160
+ raise "Don't know how to type-cast #{{ type => raw_value }.inspect }"
161
+ end
162
+ end
163
+ end
164
+
165
+ end # module Coersion
166
+
167
+ # You define your custom queries in a sub-module called Queries.
168
+ # If you don't need to redefine any of the default functionality/syntax,
169
+ # you can just create constants that point to the standard queries:
170
+ #
171
+ # SelectStatement = DataMapper::Queries::SelectStatement
172
+ #
173
+ # It's just as easy to turn that into a sub-class however:
174
+ #
175
+ # class SelectStatement < DataMapper::Queries::SelectStatement
176
+ # end
177
+ #
178
+ # You sub-class and edit instead of overwrite because you want to
179
+ # make sure your changes only affect this database adapter and avoid
180
+ # introducing incompatibilities into other adapters.
181
+ module Queries
182
+
183
+ # Your Connection class is one of two that will be almost completely custom.
184
+ # Refer to DataMapper::Queries::Connection for the required interface.
185
+ class Connection < DataMapper::Queries::Connection
186
+ end
187
+
188
+ # Reader is the other Connection related class that will be almost completely custom.
189
+ # The idea with the Reader is to avoid creating a large Array of Hash objects to
190
+ # represent rows since the hashes will be discarded almost immediately. Create only
191
+ # what you need. So the reader creates a single Hash to associate columns with their
192
+ # ordinals in the result set, then indexing the Reader for each row results in looking
193
+ # up the column index, then the value for that index in the current row array.
194
+ class Reader < DataMapper::Queries::Reader
195
+ end
196
+
197
+ class Result < DataMapper::Queries::Result
198
+ end
199
+
200
+ class DeleteStatement < DataMapper::Queries::DeleteStatement
201
+ end
202
+
203
+ class InsertStatement < DataMapper::Queries::InsertStatement
204
+ end
205
+
206
+ class SelectStatement < DataMapper::Queries::SelectStatement
207
+ end
208
+
209
+ class TruncateTableStatement < DataMapper::Queries::TruncateTableStatement
210
+ end
211
+
212
+ class UpdateStatement < DataMapper::Queries::UpdateStatement
213
+ end
214
+
215
+ class CreateTableStatement < DataMapper::Queries::CreateTableStatement
216
+ end
217
+
218
+ class DropTableStatement < DataMapper::Queries::DropTableStatement
219
+ end
220
+
221
+ class TableExistsStatement < DataMapper::Queries::TableExistsStatement
222
+ end
223
+
224
+ end # module Queries
225
+
226
+ end # class AbstractAdapter
227
+
228
+ end # module Adapters
229
+ end # module DataMapper
@@ -0,0 +1,171 @@
1
+ require 'data_mapper/adapters/abstract_adapter'
2
+ require 'data_mapper/support/connection_pool'
3
+
4
+ begin
5
+ require 'mysql'
6
+ rescue LoadError
7
+ STDERR.puts <<-EOS.gsub(/^(\s+)/, '')
8
+ This adapter currently depends on the \"mysql\" gem.
9
+ If some kind soul would like to make it work with
10
+ a pure-ruby version that'd be super spiffy.
11
+ EOS
12
+
13
+ raise
14
+ end
15
+
16
+ module DataMapper
17
+ module Adapters
18
+
19
+ class MysqlAdapter < AbstractAdapter
20
+
21
+ def initialize(configuration)
22
+ super
23
+ @connections = Support::ConnectionPool.new { Queries::Connection.new(@configuration) }
24
+ end
25
+
26
+ def connection
27
+ raise ArgumentError.new('MysqlAdapter#connection requires a block-parameter') unless block_given?
28
+ begin
29
+ @connections.hold { |connection| yield connection }
30
+ rescue Mysql::Error => me
31
+
32
+ @configuration.log.fatal(me)
33
+
34
+ @connections.available_connections.each do |sock|
35
+ begin
36
+ sock.close
37
+ rescue => se
38
+ @configuration.log.error(se)
39
+ end
40
+ end
41
+
42
+ @connections.available_connections.clear
43
+ raise me
44
+ end
45
+ end
46
+
47
+ module Quoting
48
+
49
+ def quote_table_name(name)
50
+ name.ensure_wrapped_with('`')
51
+ end
52
+
53
+ def quote_column_name(name)
54
+ name.ensure_wrapped_with('`')
55
+ end
56
+
57
+ end # module Quoting
58
+
59
+ module Coersion
60
+
61
+ def type_cast_boolean(value)
62
+ case value
63
+ when TrueClass, FalseClass then value
64
+ when "1", "true", "TRUE" then true
65
+ when "0", nil then false
66
+ else "Can't type-cast #{value.inspect} to a boolean"
67
+ end
68
+ end
69
+
70
+ def type_cast_datetime(value)
71
+ case value
72
+ when DateTime then value
73
+ when Date then DateTime.new(value)
74
+ when String then DateTime::parse(value)
75
+ else "Can't type-cast #{value.inspect} to a datetime"
76
+ end
77
+ end
78
+
79
+ end # module Coersion
80
+
81
+ module Queries
82
+
83
+ class Connection
84
+
85
+ def initialize(database)
86
+ @database = database
87
+ @dbh = Mysql.new(database.host, database.username, database.password, database.database)
88
+ database.log.debug("Initializing Connection for Database[#{database.name}]")
89
+ super(database.log)
90
+ end
91
+
92
+ def execute(statement)
93
+ send_query(statement)
94
+ Result.new(@dbh.affected_rows, @dbh.insert_id)
95
+ end
96
+
97
+ def query(statement)
98
+ Reader.new(send_query(statement))
99
+ end
100
+
101
+ def close
102
+ @dbh.close
103
+ end
104
+
105
+ private
106
+ def send_query(statement)
107
+ sql = statement.respond_to?(:to_sql) ? statement.to_sql : statement
108
+ log.debug("Database[#{@database.name}] => #{sql}")
109
+ @dbh.query(sql)
110
+ end
111
+
112
+ end # class Connection
113
+
114
+ class Reader
115
+
116
+ include Enumerable
117
+
118
+ def initialize(results)
119
+ @results = results
120
+ @columns = {}
121
+ results.fetch_fields.each_with_index do |field, index|
122
+ @columns[field.name] = index
123
+ end
124
+ @current_row_index = 0
125
+ end
126
+
127
+ def eof?
128
+ records_affected <= @current_row_index
129
+ end
130
+
131
+ def records_affected
132
+ @results.num_rows
133
+ end
134
+
135
+ def next
136
+ @current_row_index += 1
137
+ @current_row = @results.fetch_row
138
+ self
139
+ end
140
+
141
+ def each
142
+ @results.each do |row|
143
+ @current_row = row
144
+ yield self
145
+ end
146
+ end
147
+
148
+ def [](column)
149
+ index = @columns[column]
150
+ return nil if index.nil?
151
+ @current_row[index]
152
+ end
153
+
154
+ def each_pair
155
+ @columns.each_pair do |column_name, index|
156
+ yield(column_name, @current_row[index])
157
+ end
158
+ end
159
+
160
+ def close
161
+ @results.free
162
+ end
163
+
164
+ end # class Reader
165
+
166
+ end # module Queries
167
+
168
+ end # class MysqlAdapter
169
+
170
+ end # module Adapters
171
+ end # module DataMapper