datamapper 0.1.0 → 0.1.1

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