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.
- data/CHANGELOG +31 -1
- data/MIT-LICENSE +1 -1
- data/README +9 -1
- data/example.rb +23 -15
- data/lib/data_mapper.rb +5 -0
- data/lib/data_mapper/adapters/abstract_adapter.rb +9 -207
- data/lib/data_mapper/adapters/mysql_adapter.rb +132 -108
- data/lib/data_mapper/adapters/postgresql_adapter.rb +242 -0
- data/lib/data_mapper/adapters/sql/coersion.rb +74 -0
- data/lib/data_mapper/adapters/sql/commands/advanced_load_command.rb +140 -0
- data/lib/data_mapper/adapters/sql/commands/conditions.rb +161 -0
- data/lib/data_mapper/adapters/sql/commands/delete_command.rb +113 -0
- data/lib/data_mapper/adapters/sql/commands/load_command.rb +296 -0
- data/lib/data_mapper/adapters/sql/commands/save_command.rb +141 -0
- data/lib/data_mapper/adapters/sql/commands/table_exists_command.rb +33 -0
- data/lib/data_mapper/adapters/sql/mappings/column.rb +91 -0
- data/lib/data_mapper/adapters/sql/mappings/schema.rb +30 -0
- data/lib/data_mapper/adapters/sql/mappings/table.rb +143 -0
- data/lib/data_mapper/adapters/sql/quoting.rb +38 -0
- data/lib/data_mapper/adapters/sql_adapter.rb +163 -0
- data/lib/data_mapper/adapters/sqlite3_adapter.rb +155 -116
- data/lib/data_mapper/associations.rb +2 -0
- data/lib/data_mapper/associations/advanced_has_many_association.rb +55 -0
- data/lib/data_mapper/associations/belongs_to_association.rb +2 -2
- data/lib/data_mapper/associations/has_many_association.rb +3 -3
- data/lib/data_mapper/associations/has_one_association.rb +2 -2
- data/lib/data_mapper/base.rb +30 -11
- data/lib/data_mapper/callbacks.rb +4 -1
- data/lib/data_mapper/database.rb +8 -41
- data/lib/data_mapper/identity_map.rb +23 -3
- data/lib/data_mapper/session.rb +34 -186
- data/lib/data_mapper/{extensions → support}/active_record_impersonation.rb +16 -12
- data/lib/data_mapper/support/blank.rb +35 -0
- data/lib/data_mapper/support/connection_pool.rb +2 -1
- data/lib/data_mapper/support/string.rb +27 -0
- data/lib/data_mapper/support/struct.rb +26 -0
- data/lib/data_mapper/validations/unique_validator.rb +1 -3
- data/lib/data_mapper/validations/validation_helper.rb +1 -1
- data/performance.rb +24 -7
- data/profile_data_mapper.rb +24 -2
- data/rakefile.rb +2 -2
- data/spec/basic_finder.rb +2 -2
- data/spec/belongs_to.rb +1 -1
- data/spec/delete_command_spec.rb +9 -0
- data/spec/fixtures/zoos.yaml +4 -0
- data/spec/has_many.rb +1 -1
- data/spec/load_command_spec.rb +44 -0
- data/spec/models/zoo.rb +2 -0
- data/spec/save_command_spec.rb +13 -0
- data/spec/spec_helper.rb +10 -1
- data/spec/support/string_spec.rb +7 -0
- data/spec/validates_confirmation_of.rb +1 -1
- data/spec/validates_format_of.rb +1 -1
- data/spec/validates_length_of.rb +1 -1
- data/spec/validations.rb +1 -1
- metadata +23 -20
- data/lib/data_mapper/extensions/callback_helpers.rb +0 -35
- data/lib/data_mapper/loaded_set.rb +0 -45
- data/lib/data_mapper/mappings/column.rb +0 -78
- data/lib/data_mapper/mappings/schema.rb +0 -28
- data/lib/data_mapper/mappings/table.rb +0 -99
- data/lib/data_mapper/queries/conditions.rb +0 -141
- data/lib/data_mapper/queries/connection.rb +0 -34
- data/lib/data_mapper/queries/create_table_statement.rb +0 -38
- data/lib/data_mapper/queries/delete_statement.rb +0 -17
- data/lib/data_mapper/queries/drop_table_statement.rb +0 -17
- data/lib/data_mapper/queries/insert_statement.rb +0 -29
- data/lib/data_mapper/queries/reader.rb +0 -42
- data/lib/data_mapper/queries/result.rb +0 -19
- data/lib/data_mapper/queries/select_statement.rb +0 -103
- data/lib/data_mapper/queries/table_exists_statement.rb +0 -17
- data/lib/data_mapper/queries/truncate_table_statement.rb +0 -17
- 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
|