datamapper 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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,163 @@
|
|
1
|
+
require 'data_mapper/adapters/abstract_adapter'
|
2
|
+
require 'data_mapper/adapters/sql/commands/load_command'
|
3
|
+
require 'data_mapper/adapters/sql/commands/advanced_load_command'
|
4
|
+
require 'data_mapper/adapters/sql/commands/save_command'
|
5
|
+
require 'data_mapper/adapters/sql/commands/delete_command'
|
6
|
+
require 'data_mapper/adapters/sql/commands/table_exists_command'
|
7
|
+
require 'data_mapper/adapters/sql/coersion'
|
8
|
+
require 'data_mapper/adapters/sql/quoting'
|
9
|
+
require 'data_mapper/adapters/sql/mappings/schema'
|
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 SqlAdapter, and implement the
|
41
|
+
# required methods to adapt a database library for use with the DataMapper.
|
42
|
+
#
|
43
|
+
# NOTE: By inheriting from SqlAdapter, 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 SqlAdapter < AbstractAdapter
|
47
|
+
|
48
|
+
FIND_OPTIONS = [
|
49
|
+
:select, :limit, :class, :include, :reload, :conditions, :order
|
50
|
+
]
|
51
|
+
|
52
|
+
def initialize(configuration)
|
53
|
+
super
|
54
|
+
@single_threaded = configuration.single_threaded
|
55
|
+
end
|
56
|
+
|
57
|
+
def single_threaded?
|
58
|
+
@single_threaded
|
59
|
+
end
|
60
|
+
|
61
|
+
def connection(&block)
|
62
|
+
raise NotImplementedError.new
|
63
|
+
end
|
64
|
+
|
65
|
+
def transaction(&block)
|
66
|
+
raise NotImplementedError.new
|
67
|
+
end
|
68
|
+
|
69
|
+
def query(sql)
|
70
|
+
raise NotImplementedError.new
|
71
|
+
end
|
72
|
+
|
73
|
+
def schema
|
74
|
+
@schema || ( @schema = Mappings::Schema.new(self) )
|
75
|
+
end
|
76
|
+
|
77
|
+
def table_exists?(name)
|
78
|
+
self.class::Commands::TableExistsCommand.new(self, name).call
|
79
|
+
end
|
80
|
+
|
81
|
+
def delete(klass_or_instance, options = nil)
|
82
|
+
self.class::Commands::DeleteCommand.new(self, klass_or_instance, options).call
|
83
|
+
end
|
84
|
+
|
85
|
+
def save(session, instance)
|
86
|
+
self.class::Commands::SaveCommand.new(self, session, instance).call
|
87
|
+
end
|
88
|
+
|
89
|
+
def load(session, klass, options)
|
90
|
+
self.class::Commands::LoadCommand.new(self, session, klass, options).call
|
91
|
+
end
|
92
|
+
|
93
|
+
def [](klass_or_table_name)
|
94
|
+
schema[klass_or_table_name]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Escape a string of SQL with a set of arguments.
|
98
|
+
# The first argument is assumed to be the SQL to escape,
|
99
|
+
# the remaining arguments (if any) are assumed to be
|
100
|
+
# values to escape and interpolate.
|
101
|
+
#
|
102
|
+
# ==== Examples
|
103
|
+
# escape_sql("SELECT * FROM zoos")
|
104
|
+
# # => "SELECT * FROM zoos"
|
105
|
+
#
|
106
|
+
# escape_sql("SELECT * FROM zoos WHERE name = ?", "Dallas")
|
107
|
+
# # => "SELECT * FROM zoos WHERE name = `Dallas`"
|
108
|
+
#
|
109
|
+
# escape_sql("SELECT * FROM zoos WHERE name = ? AND acreage > ?", "Dallas", 40)
|
110
|
+
# # => "SELECT * FROM zoos WHERE name = `Dallas` AND acreage > 40"
|
111
|
+
#
|
112
|
+
# ==== Warning
|
113
|
+
# This method is meant mostly for adapters that don't support
|
114
|
+
# bind-parameters.
|
115
|
+
def escape_sql(*args)
|
116
|
+
sql = args.shift
|
117
|
+
|
118
|
+
unless args.empty?
|
119
|
+
sql.gsub!(/\?/) do |x|
|
120
|
+
quote_value(args.shift)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
sql
|
125
|
+
end
|
126
|
+
|
127
|
+
# This callback copies and sub-classes modules and classes
|
128
|
+
# in the AbstractAdapter to the inherited class so you don't
|
129
|
+
# have to copy and paste large blocks of code from the
|
130
|
+
# SqlAdapter.
|
131
|
+
#
|
132
|
+
# Basically, when inheriting from the AbstractAdapter, you
|
133
|
+
# aren't just inheriting a single class, you're inheriting
|
134
|
+
# a whole graph of Types. For convenience.
|
135
|
+
def self.inherited(base)
|
136
|
+
|
137
|
+
queries = base.const_set('Commands', Module.new)
|
138
|
+
|
139
|
+
Sql::Commands.constants.each do |name|
|
140
|
+
queries.const_set(name, Class.new(Sql::Commands.const_get(name)))
|
141
|
+
end
|
142
|
+
|
143
|
+
base.const_set('TYPES', TYPES.dup)
|
144
|
+
base.const_set('FIND_OPTIONS', FIND_OPTIONS.dup)
|
145
|
+
|
146
|
+
super
|
147
|
+
end
|
148
|
+
|
149
|
+
TYPES = {
|
150
|
+
:integer => 'int'.freeze,
|
151
|
+
:string => 'varchar'.freeze,
|
152
|
+
:text => 'text'.freeze,
|
153
|
+
:class => 'varchar'.freeze
|
154
|
+
}
|
155
|
+
|
156
|
+
include Sql
|
157
|
+
include Quoting
|
158
|
+
include Coersion
|
159
|
+
|
160
|
+
end # class SqlAdapter
|
161
|
+
|
162
|
+
end # module Adapters
|
163
|
+
end # module DataMapper
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'data_mapper/adapters/
|
1
|
+
require 'data_mapper/adapters/sql_adapter'
|
2
2
|
require 'data_mapper/support/connection_pool'
|
3
3
|
|
4
4
|
require 'sqlite3'
|
@@ -6,11 +6,15 @@ require 'sqlite3'
|
|
6
6
|
module DataMapper
|
7
7
|
module Adapters
|
8
8
|
|
9
|
-
class Sqlite3Adapter <
|
9
|
+
class Sqlite3Adapter < SqlAdapter
|
10
10
|
|
11
11
|
def initialize(configuration)
|
12
12
|
super
|
13
|
-
@connections = Support::ConnectionPool.new
|
13
|
+
@connections = Support::ConnectionPool.new do
|
14
|
+
dbh = SQLite3::Database.new(configuration.database)
|
15
|
+
dbh.results_as_hash = true
|
16
|
+
dbh
|
17
|
+
end
|
14
18
|
end
|
15
19
|
|
16
20
|
def connection
|
@@ -34,126 +38,76 @@ module DataMapper
|
|
34
38
|
end
|
35
39
|
end
|
36
40
|
|
41
|
+
def query(*args)
|
42
|
+
reader = connection { |db| db.query(escape_sql(*args)) }
|
43
|
+
|
44
|
+
fields = nil
|
45
|
+
rows = []
|
46
|
+
|
47
|
+
until reader.eof?
|
48
|
+
hash = reader.next
|
49
|
+
break if hash.nil?
|
50
|
+
|
51
|
+
fields = hash.keys.select { |field| field.is_a?(String) } unless fields
|
52
|
+
|
53
|
+
rows << fields.map { |field| hash[field] }
|
54
|
+
end
|
55
|
+
|
56
|
+
reader.close
|
57
|
+
|
58
|
+
struct = Support::Struct::define(fields)
|
59
|
+
|
60
|
+
rows.map do |row|
|
61
|
+
struct.new(row)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
37
65
|
TYPES.merge!({
|
38
66
|
:integer => 'INTEGER'.freeze,
|
39
67
|
:string => 'TEXT'.freeze,
|
40
68
|
:text => 'TEXT'.freeze,
|
41
69
|
:class => 'TEXT'.freeze
|
42
70
|
})
|
43
|
-
|
44
|
-
module Coersion
|
45
71
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
72
|
+
TABLE_QUOTING_CHARACTER = '"'.freeze
|
73
|
+
COLUMN_QUOTING_CHARACTER = '"'.freeze
|
74
|
+
|
75
|
+
def type_cast_boolean(value)
|
76
|
+
case value
|
77
|
+
when TrueClass, FalseClass then value
|
78
|
+
when "1", "true", "TRUE" then true
|
79
|
+
when "0", nil then false
|
80
|
+
else "Can't type-cast #{value.inspect} to a boolean"
|
53
81
|
end
|
82
|
+
end
|
54
83
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
end
|
84
|
+
def type_cast_datetime(value)
|
85
|
+
case value
|
86
|
+
when DateTime then value
|
87
|
+
when Date then DateTime.new(value)
|
88
|
+
when String then DateTime::parse(value)
|
89
|
+
else "Can't type-cast #{value.inspect} to a datetime"
|
62
90
|
end
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
module Queries
|
67
|
-
|
68
|
-
class Connection
|
69
|
-
|
70
|
-
def initialize(database)
|
71
|
-
@database = database
|
72
|
-
@dbh = SQLite3::Database.new(database.database)
|
73
|
-
database.log.debug("Initializing Connection for Database[#{database.name}]")
|
74
|
-
super(database.log)
|
75
|
-
end
|
76
|
-
|
77
|
-
def execute(statement)
|
78
|
-
send_query(statement)
|
79
|
-
Result.new(@dbh.total_changes, @dbh.last_insert_row_id)
|
80
|
-
end
|
81
|
-
|
82
|
-
def query(statement)
|
83
|
-
Reader.new(send_query(statement))
|
84
|
-
end
|
85
|
-
|
86
|
-
def close
|
87
|
-
@dbh.close
|
88
|
-
end
|
89
|
-
|
90
|
-
private
|
91
|
-
def send_query(statement)
|
92
|
-
sql = statement.respond_to?(:to_sql) ? statement.to_sql : statement
|
93
|
-
log.debug("Database[#{@database.name}] => #{sql}")
|
94
|
-
@dbh.query(sql)
|
95
|
-
end
|
96
|
-
|
97
|
-
end # class Connection
|
91
|
+
end
|
92
|
+
|
93
|
+
module Commands
|
98
94
|
|
99
|
-
class
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
def initialize(results)
|
104
|
-
@results = results
|
105
|
-
@columns = {}
|
106
|
-
@results.columns.each_with_index do |name, index|
|
107
|
-
@columns[name] = index
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def eof?
|
112
|
-
@results.eof?
|
113
|
-
end
|
114
|
-
|
115
|
-
def records_affected
|
116
|
-
@results.entries.size
|
117
|
-
end
|
118
|
-
|
119
|
-
def next
|
120
|
-
@current_row = @results.next
|
121
|
-
self
|
122
|
-
end
|
123
|
-
|
124
|
-
def each
|
125
|
-
until self.next.eof?
|
126
|
-
yield(self)
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
def [](column)
|
131
|
-
index = @columns[column]
|
132
|
-
return nil if index.nil? || @current_row.nil?
|
133
|
-
@current_row[index]
|
134
|
-
end
|
135
|
-
|
136
|
-
def each_pair
|
137
|
-
@columns.each_pair do |column_name, index|
|
138
|
-
yield(column_name, @current_row.nil? ? nil : @current_row[index])
|
139
|
-
end
|
95
|
+
class TableExistsCommand
|
96
|
+
def to_sql
|
97
|
+
"SELECT name FROM sqlite_master WHERE type = \"table\" AND name = #{table_name}"
|
140
98
|
end
|
141
99
|
|
142
|
-
def
|
143
|
-
@
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
class TableExistsStatement
|
149
|
-
def to_sql
|
150
|
-
"SELECT name FROM sqlite_master WHERE type = \"table\" AND name = #{@database.quote_value(@database[@klass].name)}"
|
100
|
+
def call
|
101
|
+
reader = @adapter.connection { |db| db.query(to_sql) }
|
102
|
+
result = reader.entries.size > 0
|
103
|
+
reader.close
|
104
|
+
result
|
151
105
|
end
|
152
|
-
end # class
|
106
|
+
end # class TableExistsCommand
|
153
107
|
|
154
|
-
class
|
155
|
-
def
|
156
|
-
table = @
|
108
|
+
class SaveCommand
|
109
|
+
def to_create_table_sql
|
110
|
+
table = @adapter[@instance]
|
157
111
|
|
158
112
|
sql = "CREATE TABLE " << table.to_sql
|
159
113
|
|
@@ -165,7 +119,7 @@ module DataMapper
|
|
165
119
|
end
|
166
120
|
|
167
121
|
def column_long_form(column)
|
168
|
-
long_form = "#{column.to_sql} #{@
|
122
|
+
long_form = "#{column.to_sql} #{@adapter.class::TYPES[column.type] || column.type}"
|
169
123
|
|
170
124
|
long_form << " NOT NULL" unless column.nullable?
|
171
125
|
long_form << " PRIMARY KEY" if column.key?
|
@@ -173,16 +127,101 @@ module DataMapper
|
|
173
127
|
|
174
128
|
return long_form
|
175
129
|
end
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
130
|
+
|
131
|
+
def execute_insert(sql)
|
132
|
+
@adapter.connection do |db|
|
133
|
+
db.query(sql)
|
134
|
+
db.last_insert_row_id
|
135
|
+
end
|
181
136
|
end
|
182
|
-
|
137
|
+
|
138
|
+
def execute_update(sql)
|
139
|
+
@adapter.connection do |db|
|
140
|
+
db.query(sql)
|
141
|
+
db.total_changes > 0
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def execute_create_table(sql)
|
146
|
+
@adapter.connection { |db| db.query(sql) }
|
147
|
+
true
|
148
|
+
end
|
149
|
+
end # class SaveCommand
|
183
150
|
|
184
|
-
|
151
|
+
class DeleteCommand
|
152
|
+
def to_truncate_sql
|
153
|
+
"DELETE FROM " << @adapter[@klass_or_instance].to_sql
|
154
|
+
end
|
155
|
+
|
156
|
+
def execute(sql)
|
157
|
+
@adapter.connection do |db|
|
158
|
+
db.query(sql)
|
159
|
+
db.total_changes > 0
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def execute_drop(sql)
|
164
|
+
@adapter.connection { |db| db.query(sql) }
|
165
|
+
true
|
166
|
+
end
|
167
|
+
end # class DeleteCommand
|
168
|
+
|
169
|
+
class LoadCommand
|
170
|
+
def eof?(reader)
|
171
|
+
reader.eof?
|
172
|
+
end
|
173
|
+
|
174
|
+
def close_reader(reader)
|
175
|
+
reader.close
|
176
|
+
end
|
177
|
+
|
178
|
+
def execute(sql)
|
179
|
+
@adapter.connection { |db| db.query(to_sql) }
|
180
|
+
end
|
181
|
+
|
182
|
+
def fetch_one(reader)
|
183
|
+
load(reader.next)
|
184
|
+
end
|
185
|
+
|
186
|
+
def fetch_all(reader)
|
187
|
+
fields = nil
|
188
|
+
rows = []
|
189
|
+
|
190
|
+
until reader.eof?
|
191
|
+
hash = reader.next
|
192
|
+
break if hash.nil?
|
193
|
+
|
194
|
+
fields = hash.keys.select { |field| field.is_a?(String) } unless fields
|
195
|
+
|
196
|
+
rows << fields.map { |name| hash[name] }
|
197
|
+
end
|
198
|
+
|
199
|
+
load_instances(fields, rows)
|
200
|
+
end
|
201
|
+
|
202
|
+
def fetch_structs(reader)
|
203
|
+
fields = nil
|
204
|
+
rows = []
|
205
|
+
|
206
|
+
until reader.eof?
|
207
|
+
hash = reader.next
|
208
|
+
break if hash.nil?
|
209
|
+
|
210
|
+
fields = hash.keys.select { |field| field.is_a?(String) } unless fields
|
211
|
+
|
212
|
+
rows << fields.map { |name| hash[name] }
|
213
|
+
end
|
214
|
+
|
215
|
+
fields = fields.inject({}) do |h,f|
|
216
|
+
h[f] = fields.index(f); h
|
217
|
+
end
|
218
|
+
|
219
|
+
load_structs(fields, rows)
|
220
|
+
end
|
221
|
+
end
|
185
222
|
|
223
|
+
end # module Commands
|
224
|
+
|
186
225
|
end # class Sqlite3Adapter
|
187
226
|
|
188
227
|
end # module Adapters
|