datamapper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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