datamapper 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. data/CHANGELOG +31 -1
  2. data/MIT-LICENSE +1 -1
  3. data/README +9 -1
  4. data/example.rb +23 -15
  5. data/lib/data_mapper.rb +5 -0
  6. data/lib/data_mapper/adapters/abstract_adapter.rb +9 -207
  7. data/lib/data_mapper/adapters/mysql_adapter.rb +132 -108
  8. data/lib/data_mapper/adapters/postgresql_adapter.rb +242 -0
  9. data/lib/data_mapper/adapters/sql/coersion.rb +74 -0
  10. data/lib/data_mapper/adapters/sql/commands/advanced_load_command.rb +140 -0
  11. data/lib/data_mapper/adapters/sql/commands/conditions.rb +161 -0
  12. data/lib/data_mapper/adapters/sql/commands/delete_command.rb +113 -0
  13. data/lib/data_mapper/adapters/sql/commands/load_command.rb +296 -0
  14. data/lib/data_mapper/adapters/sql/commands/save_command.rb +141 -0
  15. data/lib/data_mapper/adapters/sql/commands/table_exists_command.rb +33 -0
  16. data/lib/data_mapper/adapters/sql/mappings/column.rb +91 -0
  17. data/lib/data_mapper/adapters/sql/mappings/schema.rb +30 -0
  18. data/lib/data_mapper/adapters/sql/mappings/table.rb +143 -0
  19. data/lib/data_mapper/adapters/sql/quoting.rb +38 -0
  20. data/lib/data_mapper/adapters/sql_adapter.rb +163 -0
  21. data/lib/data_mapper/adapters/sqlite3_adapter.rb +155 -116
  22. data/lib/data_mapper/associations.rb +2 -0
  23. data/lib/data_mapper/associations/advanced_has_many_association.rb +55 -0
  24. data/lib/data_mapper/associations/belongs_to_association.rb +2 -2
  25. data/lib/data_mapper/associations/has_many_association.rb +3 -3
  26. data/lib/data_mapper/associations/has_one_association.rb +2 -2
  27. data/lib/data_mapper/base.rb +30 -11
  28. data/lib/data_mapper/callbacks.rb +4 -1
  29. data/lib/data_mapper/database.rb +8 -41
  30. data/lib/data_mapper/identity_map.rb +23 -3
  31. data/lib/data_mapper/session.rb +34 -186
  32. data/lib/data_mapper/{extensions → support}/active_record_impersonation.rb +16 -12
  33. data/lib/data_mapper/support/blank.rb +35 -0
  34. data/lib/data_mapper/support/connection_pool.rb +2 -1
  35. data/lib/data_mapper/support/string.rb +27 -0
  36. data/lib/data_mapper/support/struct.rb +26 -0
  37. data/lib/data_mapper/validations/unique_validator.rb +1 -3
  38. data/lib/data_mapper/validations/validation_helper.rb +1 -1
  39. data/performance.rb +24 -7
  40. data/profile_data_mapper.rb +24 -2
  41. data/rakefile.rb +2 -2
  42. data/spec/basic_finder.rb +2 -2
  43. data/spec/belongs_to.rb +1 -1
  44. data/spec/delete_command_spec.rb +9 -0
  45. data/spec/fixtures/zoos.yaml +4 -0
  46. data/spec/has_many.rb +1 -1
  47. data/spec/load_command_spec.rb +44 -0
  48. data/spec/models/zoo.rb +2 -0
  49. data/spec/save_command_spec.rb +13 -0
  50. data/spec/spec_helper.rb +10 -1
  51. data/spec/support/string_spec.rb +7 -0
  52. data/spec/validates_confirmation_of.rb +1 -1
  53. data/spec/validates_format_of.rb +1 -1
  54. data/spec/validates_length_of.rb +1 -1
  55. data/spec/validations.rb +1 -1
  56. metadata +23 -20
  57. data/lib/data_mapper/extensions/callback_helpers.rb +0 -35
  58. data/lib/data_mapper/loaded_set.rb +0 -45
  59. data/lib/data_mapper/mappings/column.rb +0 -78
  60. data/lib/data_mapper/mappings/schema.rb +0 -28
  61. data/lib/data_mapper/mappings/table.rb +0 -99
  62. data/lib/data_mapper/queries/conditions.rb +0 -141
  63. data/lib/data_mapper/queries/connection.rb +0 -34
  64. data/lib/data_mapper/queries/create_table_statement.rb +0 -38
  65. data/lib/data_mapper/queries/delete_statement.rb +0 -17
  66. data/lib/data_mapper/queries/drop_table_statement.rb +0 -17
  67. data/lib/data_mapper/queries/insert_statement.rb +0 -29
  68. data/lib/data_mapper/queries/reader.rb +0 -42
  69. data/lib/data_mapper/queries/result.rb +0 -19
  70. data/lib/data_mapper/queries/select_statement.rb +0 -103
  71. data/lib/data_mapper/queries/table_exists_statement.rb +0 -17
  72. data/lib/data_mapper/queries/truncate_table_statement.rb +0 -17
  73. data/lib/data_mapper/queries/update_statement.rb +0 -25
@@ -0,0 +1,242 @@
1
+ require 'data_mapper/adapters/sql_adapter'
2
+ require 'data_mapper/support/connection_pool'
3
+
4
+ begin
5
+ require 'postgres'
6
+ rescue LoadError
7
+ STDERR.puts <<-EOS.gsub(/^(\s+)/, '')
8
+ This adapter currently depends on the \"postgres\" 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 PostgresqlAdapter < SqlAdapter
20
+
21
+ def initialize(configuration)
22
+ super
23
+ # Initialize the connection pool.
24
+ @connections = Support::ConnectionPool.new do
25
+ # add port parameter to configuration
26
+ pg_conn = PGconn.connect(configuration.host, 5432, "", "", configuration.database, configuration.username, configuration.password)
27
+ end
28
+ end
29
+
30
+
31
+ # Returns an available connection. Flushes the connection-pool if
32
+ # the connection returns an error.
33
+ def connection
34
+ raise ArgumentError.new('PostgresqlAdapter#connection requires a block-parameter') unless block_given?
35
+ begin
36
+ @connections.hold { |connection| yield connection }
37
+ rescue PGError => me
38
+
39
+ @configuration.log.fatal(me)
40
+
41
+ @connections.available_connections.each do |sock|
42
+ begin
43
+ sock.close
44
+ rescue => se
45
+ @configuration.log.error(se)
46
+ end
47
+ end
48
+
49
+ @connections.available_connections.clear
50
+ raise me
51
+ end
52
+ end
53
+
54
+ def query(*args)
55
+ pg_result = connection { |db| db.exec(escape_sql(*args)) }
56
+
57
+ struct = Struct.new(*pg_result.fields.map { |field| Inflector.underscore(field).to_sym })
58
+ results = []
59
+
60
+ pg_result.each do |row|
61
+ results << struct.new(*row)
62
+ end
63
+
64
+ pg_result.clear
65
+ return results
66
+ end
67
+
68
+ TABLE_QUOTING_CHARACTER = '"'.freeze
69
+ COLUMN_QUOTING_CHARACTER = '"'.freeze
70
+
71
+ def type_cast_boolean(value)
72
+ case value
73
+ when TrueClass, FalseClass then value
74
+ when "t", "true", "TRUE" then true
75
+ when "f", nil then false
76
+ else "Can't type-cast #{value.inspect} to a boolean"
77
+ end
78
+ end
79
+
80
+ def type_cast_datetime(value)
81
+ case value
82
+ when DateTime then value
83
+ when Date then DateTime.new(value)
84
+ when String then DateTime::parse(value)
85
+ else "Can't type-cast #{value.inspect} to a datetime"
86
+ end
87
+ end
88
+
89
+ def sequence_name(table)
90
+ quote_table_name(table.to_sql.gsub('"', '') + "_id_seq")
91
+ end
92
+
93
+ TYPES.merge!({
94
+ :integer => 'integer'.freeze,
95
+ :string => 'varchar'.freeze,
96
+ :text => 'text'.freeze,
97
+ :class => 'varchar'.freeze,
98
+ :datetime => 'timestamp with time zone'.freeze
99
+ })
100
+
101
+ module Commands
102
+
103
+ class TableExistsCommand
104
+ def to_sql
105
+ # TODO cache this somewhere
106
+ schema_list = @adapter.connection { |db| db.exec('SHOW search_path').result[0][0].split(',').collect { |s| "'#{s}'" }.join(',') }
107
+ "SELECT tablename FROM pg_tables WHERE schemaname IN (#{schema_list}) AND tablename = #{table_name}"
108
+ end
109
+
110
+ def call
111
+ sql = to_sql
112
+ @adapter.log.debug(sql)
113
+ reader = @adapter.connection { |db| db.exec(sql) }
114
+ result = reader.entries.size > 0
115
+ reader.clear
116
+ result
117
+ end
118
+ end
119
+
120
+ class DeleteCommand
121
+
122
+ def execute(sql)
123
+ @adapter.connection do |db|
124
+ @adapter.log.debug(sql)
125
+ db.exec(sql).status == PGresult::COMMAND_OK
126
+ end
127
+ end
128
+
129
+ def to_truncate_sql
130
+ table = @adapter[@klass_or_instance]
131
+ sequence = @adapter.sequence_name(table)
132
+ # truncate the table and reset the sequence value
133
+ "DELETE FROM " << table.to_sql << "; SELECT setval('#{sequence}', (SELECT COALESCE(MAX(id)+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{table.to_sql}), false)"
134
+ end
135
+
136
+ def execute_drop(sql)
137
+ @adapter.log.debug(sql)
138
+ @adapter.connection { |db| db.exec(sql) }
139
+ true
140
+ end
141
+
142
+ end
143
+
144
+ class SaveCommand
145
+
146
+ def execute_insert(sql)
147
+ @adapter.connection do |db|
148
+ @adapter.log.debug(sql)
149
+ db.exec(sql)
150
+ # Current id or latest value read from sequence in this session
151
+ # See: http://www.postgresql.org/docs/8.1/interactive/functions-sequence.html
152
+ @instance.key || db.exec("SELECT last_value from #{@adapter.sequence_name(@adapter[@instance.class])}")[0][0]
153
+ end
154
+ end
155
+
156
+ def execute_update(sql)
157
+ @adapter.connection do |db|
158
+ @adapter.log.debug(sql)
159
+ db.exec(sql).cmdstatus.split(' ').last.to_i > 0
160
+ end
161
+ end
162
+
163
+ def execute_create_table(sql)
164
+ @adapter.log.debug(sql)
165
+ @adapter.connection { |db| db.exec(sql) }
166
+ true
167
+ end
168
+
169
+ def to_create_table_sql
170
+ table = @adapter[@instance]
171
+
172
+ sql = "CREATE TABLE " << table.to_sql
173
+
174
+ sql << " (" << table.columns.map do |column|
175
+ column_long_form(column)
176
+ end.join(', ') << ")"
177
+
178
+ return sql
179
+ end
180
+
181
+ def column_long_form(column)
182
+
183
+ long_form = if column.key?
184
+ "#{column.to_sql} serial primary key"
185
+ else
186
+ "#{column.to_sql} #{@adapter.class::TYPES[column.type] || column.type}"
187
+ end
188
+ long_form << " NOT NULL" unless column.nullable?
189
+ long_form << " default #{column.options[:default]}" if column.options.has_key?(:default)
190
+
191
+ return long_form
192
+ end
193
+ end
194
+
195
+ class LoadCommand
196
+ def eof?(pg_result)
197
+ pg_result.result.entries.empty?
198
+ end
199
+
200
+ def close_reader(pg_result)
201
+ pg_result.clear
202
+ end
203
+
204
+ def execute(sql)
205
+ @adapter.log.debug(sql)
206
+ @adapter.connection { |db| db.exec(to_sql) }
207
+ end
208
+
209
+ def fetch_one(pg_result)
210
+ load(process_row(columns(pg_result), pg_result.result[0]))
211
+ end
212
+
213
+ def fetch_all(pg_result)
214
+ load_instances(pg_result.fields, pg_result)
215
+ end
216
+
217
+ private
218
+
219
+ def columns(pg_result)
220
+ columns = {}
221
+ pg_result.fields.each_with_index do |name, index|
222
+ columns[name] = index
223
+ end
224
+ columns
225
+ end
226
+
227
+ def process_row(columns, row)
228
+ hash = {}
229
+ columns.each_pair do |name,index|
230
+ hash[name] = row[index]
231
+ end
232
+ hash
233
+ end
234
+
235
+ end
236
+
237
+ end
238
+
239
+ end # class PostgresqlAdapter
240
+
241
+ end # module Adapters
242
+ end # module DataMapper
@@ -0,0 +1,74 @@
1
+ module DataMapper
2
+ module Adapters
3
+ module Sql
4
+ # Coersion is a mixin that allows for coercing database values to Ruby Types.
5
+ #
6
+ # DESIGN: Probably should handle the opposite scenario here too. I believe that's
7
+ # currently in DataMapper::Database, which is obviously not a very good spot for
8
+ # it.
9
+ module Coersion
10
+
11
+ TRUE_ALIASES = ['true'.freeze, 'TRUE'.freeze]
12
+ FALSE_ALIASES = [nil]
13
+
14
+ def self.included(base)
15
+ base.const_set('TRUE_ALIASES', TRUE_ALIASES.dup)
16
+ base.const_set('FALSE_ALIASES', FALSE_ALIASES.dup)
17
+ end
18
+
19
+ def type_cast_boolean(raw_value)
20
+ case raw_value
21
+ when TrueClass, FalseClass then raw_value
22
+ when *self::class::TRUE_ALIASES then true
23
+ when *self::class::FALSE_ALIASES then false
24
+ else "Can't type-cast #{value.inspect} to a boolean"
25
+ end
26
+ end
27
+
28
+ def type_cast_string(raw_value)
29
+ return nil if raw_value.blank?
30
+ raw_value
31
+ end
32
+
33
+ def type_cast_text(raw_value)
34
+ return nil if raw_value.blank?
35
+ raw_value
36
+ end
37
+
38
+ def type_cast_class(raw_value)
39
+ return nil if raw_value.blank?
40
+ Kernel::const_get(raw_value)
41
+ end
42
+
43
+ def type_cast_integer(raw_value)
44
+ return nil if raw_value.blank?
45
+ raw_value.to_i # Integer(raw_value) would be "safer", but not as fast.
46
+ rescue ArgumentError
47
+ nil
48
+ end
49
+
50
+ def type_cast_datetime(raw_value)
51
+ return nil if raw_value.blank?
52
+
53
+ case raw_value
54
+ when DateTime then raw_value
55
+ when Date then DateTime.new(raw_value)
56
+ when String then DateTime::parse(raw_value)
57
+ else "Can't type-cast #{raw_value.inspect} to a datetime"
58
+ end
59
+ end
60
+
61
+ def type_cast_value(type, raw_value)
62
+ return nil if raw_value.blank?
63
+
64
+ if respond_to?("type_cast_#{type}")
65
+ send("type_cast_#{type}", raw_value)
66
+ else
67
+ raise "Don't know how to type-cast #{{ type => raw_value }.inspect }"
68
+ end
69
+ end
70
+
71
+ end # module Coersion
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,140 @@
1
+ module DataMapper
2
+ module Adapters
3
+ module Sql
4
+ module Commands
5
+
6
+ class AdvancedLoadCommand
7
+
8
+ attr_reader :conditions, :options
9
+
10
+ def initialize(adapter, session, primary_class, options = {})
11
+ @adapter, @session, @primary_class, @options = adapter, session, primary_class, options
12
+
13
+ @order = @options[:order]
14
+ @limit = @options[:limit]
15
+ @offset = @options[:offset]
16
+ @reload = @options[:reload]
17
+ @include = @options[:include]
18
+ @instance_id = @options[:id]
19
+ @conditions = @options[:conditions]
20
+ @join_fetch = false
21
+ @joins = []
22
+ end
23
+
24
+ # If +true+ then force the command to reload any objects
25
+ # already existing in the IdentityMap when executing.
26
+ def reload?
27
+ @reload
28
+ end
29
+
30
+ # Determine if there is a limitation on the number of
31
+ # instances returned in the results. If +nil+, no limit
32
+ # is set. Can be used in conjunction with #offset for
33
+ # paging through a set of results.
34
+ def limit
35
+ @limit
36
+ end
37
+
38
+ # Used in conjunction with #limit to page through a set
39
+ # of results.
40
+ def offset
41
+ @offset
42
+ end
43
+
44
+ # Generate a select statement based on the initialization
45
+ # arguments.
46
+ def to_sql
47
+ sql = 'SELECT ' << columns_for_select.join(', ')
48
+ sql << ' FROM ' << from_table_name
49
+
50
+ if @join_fetch
51
+ @joins.each do |entry|
52
+ primary_table, association_table, association = entry
53
+ sql << ' JOIN ' << association_table.to_sql << ' ON '
54
+ sql << association_table.to_sql << '.'
55
+ sql << @adapter.quote_column_name(association.foreign_key)
56
+ sql << ' = ' << primary_table.key.to_sql(true)
57
+ end
58
+ end
59
+
60
+ return sql
61
+ end
62
+
63
+ private
64
+
65
+ # Return the Sql-escaped columns names to be selected in the results.
66
+ def columns_for_select
67
+ @columns_for_select || @columns_for_select = begin
68
+ columns.map { |column| column.to_sql(@join_fetch) }
69
+ end
70
+ end
71
+
72
+ # Returns the DataMapper::Adapters::Sql::Mappings::Column instances to
73
+ # be selected in the results.
74
+ def columns
75
+ @columns || begin
76
+ @columns = if @options.has_key?(:select)
77
+ primary_class_table.columns.select do |column|
78
+ @options[:select].include?(column.name)
79
+ end
80
+ else
81
+ primary_class_table.columns.select do |column|
82
+ include_column?(column.name) || !column.lazy?
83
+ end
84
+ end
85
+
86
+ # Return if there is no +:include+ option to evaluate.
87
+ return @columns if @include.nil?
88
+
89
+ if @include.kind_of?(Array)
90
+ # Return if all +:include+ parameters are columns in
91
+ # the primary_class_table.
92
+ return @columns if @include.all? do |name|
93
+ !primary_class_table[name].nil?
94
+ end
95
+ elsif @include.kind_of?(Symbol)
96
+ # Return if the include is a column in the primary_class_table.
97
+ return @columns if primary_class_table[@include]
98
+
99
+ primary_class_table.associations.each do |association|
100
+ next unless association.name == @include
101
+ association_table = @adapter[association.constant]
102
+ @columns += association_table.columns
103
+ @joins << [ primary_class_table, association_table, association ]
104
+ end
105
+
106
+ @join_fetch = true
107
+ else
108
+ raise ':include option must be a Symbol or Array of Symbols'
109
+ end
110
+
111
+ @columns
112
+ end
113
+ end
114
+
115
+ # Determine if a Column should be included based on the
116
+ # value of the +:include+ option.
117
+ def include_column?(name)
118
+ case @include
119
+ when nil then false
120
+ when Symbol then @include == name
121
+ when Array then @include.includes?(name)
122
+ else raise ':include option must be a Symbol or Array of Symbols'
123
+ end
124
+ end
125
+
126
+ # Return the Sql-escaped table name of the +primary_class+.
127
+ def from_table_name
128
+ @from_table_name || (@from_table_name = @adapter[@primary_class].to_sql)
129
+ end
130
+
131
+ # Returns the DataMapper::Adapters::Sql::Mappings::Table for the +primary_class+.
132
+ def primary_class_table
133
+ @primary_class_table || (@primary_class_table = @adapter[@primary_class])
134
+ end
135
+
136
+ end # class LoadCommand
137
+ end # module Commands
138
+ end # module Sql
139
+ end # module Adapters
140
+ end # module DataMapper