datamapper 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +65 -0
- data/README +193 -1
- data/do_performance.rb +153 -0
- data/environment.rb +45 -0
- data/example.rb +119 -22
- data/lib/data_mapper.rb +36 -16
- data/lib/data_mapper/adapters/abstract_adapter.rb +8 -0
- data/lib/data_mapper/adapters/data_object_adapter.rb +360 -0
- data/lib/data_mapper/adapters/mysql_adapter.rb +30 -179
- data/lib/data_mapper/adapters/postgresql_adapter.rb +90 -199
- data/lib/data_mapper/adapters/sql/coersion.rb +32 -3
- data/lib/data_mapper/adapters/sql/commands/conditions.rb +97 -128
- data/lib/data_mapper/adapters/sql/commands/load_command.rb +234 -231
- data/lib/data_mapper/adapters/sql/commands/loader.rb +99 -0
- data/lib/data_mapper/adapters/sql/mappings/associations_set.rb +30 -0
- data/lib/data_mapper/adapters/sql/mappings/column.rb +68 -6
- data/lib/data_mapper/adapters/sql/mappings/schema.rb +6 -3
- data/lib/data_mapper/adapters/sql/mappings/table.rb +71 -42
- data/lib/data_mapper/adapters/sql/quoting.rb +8 -2
- data/lib/data_mapper/adapters/sqlite3_adapter.rb +32 -201
- data/lib/data_mapper/associations.rb +21 -7
- data/lib/data_mapper/associations/belongs_to_association.rb +96 -80
- data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +158 -67
- data/lib/data_mapper/associations/has_many_association.rb +96 -78
- data/lib/data_mapper/associations/has_n_association.rb +64 -0
- data/lib/data_mapper/associations/has_one_association.rb +49 -79
- data/lib/data_mapper/associations/reference.rb +47 -0
- data/lib/data_mapper/base.rb +216 -50
- data/lib/data_mapper/callbacks.rb +71 -24
- data/lib/data_mapper/{session.rb → context.rb} +20 -8
- data/lib/data_mapper/database.rb +176 -45
- data/lib/data_mapper/embedded_value.rb +65 -0
- data/lib/data_mapper/identity_map.rb +12 -4
- data/lib/data_mapper/support/active_record_impersonation.rb +12 -8
- data/lib/data_mapper/support/enumerable.rb +8 -0
- data/lib/data_mapper/support/serialization.rb +13 -0
- data/lib/data_mapper/support/string.rb +1 -12
- data/lib/data_mapper/support/symbol.rb +3 -0
- data/lib/data_mapper/validations/unique_validator.rb +1 -2
- data/lib/data_mapper/validations/validation_helper.rb +18 -1
- data/performance.rb +109 -34
- data/plugins/can_has_sphinx/LICENSE +23 -0
- data/plugins/can_has_sphinx/README +4 -0
- data/plugins/can_has_sphinx/REVISION +1 -0
- data/plugins/can_has_sphinx/Rakefile +22 -0
- data/plugins/can_has_sphinx/init.rb +1 -0
- data/plugins/can_has_sphinx/install.rb +1 -0
- data/plugins/can_has_sphinx/lib/acts_as_sphinx.rb +123 -0
- data/plugins/can_has_sphinx/lib/sphinx.rb +460 -0
- data/plugins/can_has_sphinx/scripts/sphinx.sh +47 -0
- data/plugins/can_has_sphinx/tasks/acts_as_sphinx_tasks.rake +41 -0
- data/plugins/dataobjects/REVISION +1 -0
- data/plugins/dataobjects/Rakefile +7 -0
- data/plugins/dataobjects/do.rb +246 -0
- data/plugins/dataobjects/do_mysql.rb +179 -0
- data/plugins/dataobjects/do_postgres.rb +181 -0
- data/plugins/dataobjects/do_sqlite3.rb +153 -0
- data/plugins/dataobjects/spec/do_spec.rb +150 -0
- data/plugins/dataobjects/spec/spec_helper.rb +81 -0
- data/plugins/dataobjects/swig_mysql/do_mysql.bundle +0 -0
- data/plugins/dataobjects/swig_mysql/extconf.rb +33 -0
- data/plugins/dataobjects/swig_mysql/mysql_c.c +18800 -0
- data/plugins/dataobjects/swig_mysql/mysql_c.i +8 -0
- data/plugins/dataobjects/swig_mysql/mysql_supp.i +46 -0
- data/plugins/dataobjects/swig_postgres/Makefile +146 -0
- data/plugins/dataobjects/swig_postgres/extconf.rb +29 -0
- data/plugins/dataobjects/swig_postgres/postgres_c.bundle +0 -0
- data/plugins/dataobjects/swig_postgres/postgres_c.c +8185 -0
- data/plugins/dataobjects/swig_postgres/postgres_c.i +73 -0
- data/plugins/dataobjects/swig_sqlite/db +0 -0
- data/plugins/dataobjects/swig_sqlite/extconf.rb +9 -0
- data/plugins/dataobjects/swig_sqlite/sqlite3_c.c +4725 -0
- data/plugins/dataobjects/swig_sqlite/sqlite_c.i +168 -0
- data/rakefile.rb +45 -23
- data/spec/acts_as_tree_spec.rb +39 -0
- data/spec/associations_spec.rb +220 -0
- data/spec/attributes_spec.rb +15 -0
- data/spec/base_spec.rb +44 -0
- data/spec/callbacks_spec.rb +45 -0
- data/spec/can_has_sphinx.rb +6 -0
- data/spec/coersion_spec.rb +34 -0
- data/spec/conditions_spec.rb +49 -0
- data/spec/conversions_to_yaml_spec.rb +17 -0
- data/spec/count_command_spec.rb +11 -0
- data/spec/delete_command_spec.rb +1 -1
- data/spec/embedded_value_spec.rb +23 -0
- data/spec/fixtures/animals_exhibits.yaml +2 -0
- data/spec/fixtures/people.yaml +18 -1
- data/spec/{legacy.rb → legacy_spec.rb} +3 -3
- data/spec/load_command_spec.rb +157 -20
- data/spec/magic_columns_spec.rb +9 -0
- data/spec/mock_adapter.rb +20 -0
- data/spec/models/animal.rb +1 -1
- data/spec/models/animals_exhibit.rb +6 -0
- data/spec/models/exhibit.rb +2 -0
- data/spec/models/person.rb +26 -1
- data/spec/models/project.rb +19 -0
- data/spec/models/sales_person.rb +1 -0
- data/spec/models/section.rb +6 -0
- data/spec/models/zoo.rb +3 -1
- data/spec/query_spec.rb +9 -0
- data/spec/save_command_spec.rb +65 -1
- data/spec/schema_spec.rb +89 -0
- data/spec/single_table_inheritance_spec.rb +27 -0
- data/spec/spec_helper.rb +9 -55
- data/spec/{symbolic_operators.rb → symbolic_operators_spec.rb} +9 -5
- data/spec/{validates_confirmation_of.rb → validates_confirmation_of_spec.rb} +4 -3
- data/spec/{validates_format_of.rb → validates_format_of_spec.rb} +5 -4
- data/spec/{validates_length_of.rb → validates_length_of_spec.rb} +8 -7
- data/spec/{validates_uniqueness_of.rb → validates_uniqueness_of_spec.rb} +7 -10
- data/spec/{validations.rb → validations_spec.rb} +24 -6
- data/tasks/drivers.rb +20 -0
- data/tasks/fixtures.rb +42 -0
- metadata +181 -42
- data/lib/data_mapper/adapters/sql/commands/advanced_load_command.rb +0 -140
- data/lib/data_mapper/adapters/sql/commands/delete_command.rb +0 -113
- data/lib/data_mapper/adapters/sql/commands/save_command.rb +0 -141
- data/lib/data_mapper/adapters/sql/commands/table_exists_command.rb +0 -33
- data/lib/data_mapper/adapters/sql_adapter.rb +0 -163
- data/lib/data_mapper/associations/advanced_has_many_association.rb +0 -55
- data/lib/data_mapper/support/blank_slate.rb +0 -3
- data/lib/data_mapper/support/proc.rb +0 -69
- data/lib/data_mapper/support/struct.rb +0 -26
- data/lib/data_mapper/unit_of_work.rb +0 -38
- data/spec/basic_finder.rb +0 -67
- data/spec/belongs_to.rb +0 -47
- data/spec/has_and_belongs_to_many.rb +0 -25
- data/spec/has_many.rb +0 -34
- data/spec/new_record.rb +0 -24
- data/spec/sub_select.rb +0 -16
- data/spec/support/string_spec.rb +0 -7
data/example.rb
CHANGED
@@ -1,33 +1,130 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
3
|
+
ENV['LOG_NAME'] = 'example'
|
4
|
+
require 'environment'
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
unless ENV['LOGGER'] == 'false'
|
11
|
-
log_stream 'example.log'
|
12
|
-
log_level Logger::DEBUG
|
13
|
-
end
|
6
|
+
# Define a fixtures helper method to load up our test data.
|
7
|
+
def fixtures(name, force = false)
|
8
|
+
entry = YAML::load_file(File.dirname(__FILE__) + "/spec/fixtures/#{name}.yaml")
|
9
|
+
klass = Kernel::const_get(Inflector.classify(Inflector.singularize(name)))
|
14
10
|
|
15
|
-
|
11
|
+
klass.auto_migrate!
|
16
12
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
database_name << '.db'
|
13
|
+
(entry.kind_of?(Array) ? entry : [entry]).each do |hash|
|
14
|
+
if hash['type']
|
15
|
+
Object::const_get(hash['type'])::create(hash)
|
16
|
+
else
|
17
|
+
klass::create(hash)
|
18
|
+
end
|
24
19
|
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Pre-fill the database so non-destructive tests don't need to reload fixtures.
|
23
|
+
Dir[File.dirname(__FILE__) + "/spec/fixtures/*.yaml"].each do |path|
|
24
|
+
fixtures(File::basename(path).sub(/\.yaml$/, ''), true)
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'irb'
|
28
|
+
|
29
|
+
database { IRB::start }
|
30
|
+
|
31
|
+
if false
|
32
|
+
|
33
|
+
# Simple example to setup a database:
|
34
|
+
DataMapper::Database.setup({
|
35
|
+
:adapter => 'mysql',
|
36
|
+
:database => 'data_mapper_1',
|
37
|
+
:username => 'root'
|
38
|
+
})
|
39
|
+
|
40
|
+
class Animal < DataMapper::Base
|
41
|
+
set_table_name 'animals' # Just as an example. Same inflector as Rails,
|
42
|
+
# so this really isn't necessary.
|
43
|
+
|
44
|
+
property :name, :string
|
45
|
+
property :notes, :string, :lazy => true
|
25
46
|
|
26
|
-
|
47
|
+
has_and_belongs_to_many :exhibits
|
48
|
+
end
|
49
|
+
|
50
|
+
class Exhibit < DataMapper::Base
|
51
|
+
property :name, :string
|
52
|
+
belongs_to :zoo
|
53
|
+
end
|
54
|
+
|
55
|
+
class Zoo < DataMapper::Base
|
56
|
+
property :name, :string
|
57
|
+
has_many :exhibits
|
27
58
|
end
|
28
59
|
|
29
|
-
|
30
|
-
|
60
|
+
class Person < DataMapper::Base
|
61
|
+
|
62
|
+
property :name, :string
|
63
|
+
property :age, :integer
|
64
|
+
property :occupation, :string
|
65
|
+
property :notes, :text, :lazy => true
|
66
|
+
|
67
|
+
# Generates Person::Address class:
|
68
|
+
embed :address do
|
69
|
+
property :street, :string
|
70
|
+
property :city, :string
|
71
|
+
property :state, :string, :size => 2
|
72
|
+
property :postal_code, :string
|
73
|
+
end
|
31
74
|
end
|
32
75
|
|
33
|
-
#
|
76
|
+
# Compatible with ActiveRecord finder syntax:
|
77
|
+
Zoo.find(1)
|
78
|
+
Zoo.find(:first, :conditions => ['name = ?', 'Galveston'])
|
79
|
+
Zoo.find(:all)
|
80
|
+
|
81
|
+
# These are options as well:
|
82
|
+
Zoo[1]
|
83
|
+
Zoo.first(:name => 'Galveston')
|
84
|
+
Zoo.all
|
85
|
+
|
86
|
+
# Or even this as an alias to ::first:
|
87
|
+
Zoo[:name => 'Galveston']
|
88
|
+
|
89
|
+
# EmbeddedValues are just nice sugar to partition
|
90
|
+
# denormalized data.
|
91
|
+
Person.first.address.city
|
92
|
+
|
93
|
+
# Remove all data in a table...
|
94
|
+
Person.truncate!
|
95
|
+
|
96
|
+
# Create a new object...
|
97
|
+
Person::create(:name => 'Sam', :age => 30, :occupation => 'Software Monkey')
|
98
|
+
|
99
|
+
# Saving only updates the values that have changed,
|
100
|
+
# and is skipped entirely if the object is not dirty.
|
101
|
+
dumbo = Animal.first(:name => 'Elephant')
|
102
|
+
dumbo.notes = 'He can fly!'
|
103
|
+
dumbo.save # returns true
|
104
|
+
dumbo.save # The object is no longer dirty, so returns false
|
105
|
+
|
106
|
+
# DataMapper associations are loaded as sets.
|
107
|
+
# Here's the code:
|
108
|
+
Zoo.all.each { |zoo| zoo.exhibits.entries }
|
109
|
+
# The important bit to understand about the above is that
|
110
|
+
# every Zoo that was loaded by Zoo.all has a reference to
|
111
|
+
# every other Zoo it was loaded with through Zoo#loaded_set.
|
112
|
+
# This is then used to load all other instances in the set
|
113
|
+
# when the association of one instance is accessed. So while
|
114
|
+
# it looks like we'd run into the dreaded 1+N query problem
|
115
|
+
# with the above, we actually avoid it entirely. The above
|
116
|
+
# code will only execute two queries. The first to find all
|
117
|
+
# zoos, the second to load all exhibits with zoo_id's that
|
118
|
+
# are a part of the set of loaded zoos.
|
119
|
+
|
120
|
+
|
121
|
+
# Objects within the same session are uniqued, so this is both
|
122
|
+
# faster, and fulfills obvious expectations.
|
123
|
+
database do
|
124
|
+
Zoo.first == Zoo.first
|
125
|
+
end
|
126
|
+
|
127
|
+
# DataMapper find_by_sql equivilent
|
128
|
+
database.query("SELECT * FROM zoos")
|
129
|
+
|
130
|
+
end
|
data/lib/data_mapper.rb
CHANGED
@@ -1,35 +1,55 @@
|
|
1
|
+
# This file begins the loading sequence.
|
2
|
+
#
|
3
|
+
# Quick Overview:
|
4
|
+
# * Requires set, fastthread, support libs, and base
|
5
|
+
# * Sets the applications root and environment for compatibility with rails or merb
|
6
|
+
# * Checks for the database.yml and loads it if it exists
|
7
|
+
# * Sets up the database using the config from the yaml file or from the environment
|
8
|
+
# *
|
9
|
+
|
1
10
|
# This line just let's us require anything in the +lib+ sub-folder
|
2
11
|
# without specifying a full path.
|
12
|
+
|
3
13
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
4
14
|
|
15
|
+
DM_PLUGINS_ROOT = (File.dirname(__FILE__) + '/../plugins')
|
16
|
+
|
5
17
|
# Require the basics...
|
18
|
+
require 'yaml'
|
6
19
|
require 'set'
|
7
20
|
require 'fastthread'
|
8
21
|
require 'data_mapper/support/blank'
|
9
22
|
require 'data_mapper/support/enumerable'
|
10
23
|
require 'data_mapper/support/symbol'
|
11
24
|
require 'data_mapper/support/string'
|
12
|
-
require 'data_mapper/support/proc'
|
13
25
|
require 'data_mapper/support/inflector'
|
14
|
-
require 'data_mapper/support/struct'
|
15
26
|
require 'data_mapper/database'
|
16
27
|
require 'data_mapper/base'
|
17
28
|
|
18
|
-
# This block of code is for compatibility with Ruby On Rails' database.yml
|
29
|
+
# This block of code is for compatibility with Ruby On Rails' or Merb's database.yml
|
19
30
|
# file, allowing you to simply require the data_mapper.rb in your
|
20
31
|
# Rails application's environment.rb to configure the DataMapper.
|
21
|
-
|
22
|
-
|
32
|
+
|
33
|
+
application_root, application_environment = *if defined?(MERB_ROOT)
|
34
|
+
[MERB_ROOT, MERB_ENV]
|
35
|
+
elsif defined?(RAILS_ROOT)
|
36
|
+
[RAILS_ROOT, RAILS_ENV]
|
37
|
+
end
|
38
|
+
|
39
|
+
DM_APP_ROOT = application_root || Dir::pwd
|
40
|
+
|
41
|
+
if application_root && File.exists?(application_root + '/config/database.yml')
|
42
|
+
|
43
|
+
database_configurations = YAML::load_file(application_root + '/config/database.yml')
|
44
|
+
current_database_config = database_configurations[application_environment] || database_configurations[application_environment.to_sym]
|
45
|
+
|
46
|
+
default_database_config = {
|
47
|
+
:adapter => current_database_config['adapter'] || current_database_config[:adapter],
|
48
|
+
:host => current_database_config['host'] || current_database_config[:host],
|
49
|
+
:database => current_database_config['database'] || current_database_config[:database],
|
50
|
+
:username => current_database_config['username'] || current_database_config[:username],
|
51
|
+
:password => current_database_config['password'] || current_database_config[:password]
|
52
|
+
}
|
23
53
|
|
24
|
-
|
25
|
-
current_config = rails_config[RAILS_ENV.to_s]
|
26
|
-
|
27
|
-
DataMapper::Database.setup do
|
28
|
-
adapter current_config['adapter']
|
29
|
-
host current_config['host']
|
30
|
-
database current_config['database']
|
31
|
-
username current_config['username']
|
32
|
-
password current_config['password']
|
33
|
-
cache WeakHash::Factory
|
34
|
-
end
|
54
|
+
DataMapper::Database.setup(default_database_config)
|
35
55
|
end
|
@@ -9,6 +9,14 @@ module DataMapper
|
|
9
9
|
@configuration = configuration
|
10
10
|
end
|
11
11
|
|
12
|
+
def index_path
|
13
|
+
@configuration.index_path
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
@configuration.name
|
18
|
+
end
|
19
|
+
|
12
20
|
def delete(instance_or_klass, options = nil)
|
13
21
|
raise NotImplementedError.new
|
14
22
|
end
|
@@ -0,0 +1,360 @@
|
|
1
|
+
require 'data_mapper/adapters/abstract_adapter'
|
2
|
+
require 'data_mapper/adapters/sql/commands/load_command'
|
3
|
+
require 'data_mapper/adapters/sql/coersion'
|
4
|
+
require 'data_mapper/adapters/sql/quoting'
|
5
|
+
require 'data_mapper/adapters/sql/mappings/schema'
|
6
|
+
require 'data_mapper/support/connection_pool'
|
7
|
+
|
8
|
+
module DataMapper
|
9
|
+
|
10
|
+
# An Adapter is really a Factory for three types of object,
|
11
|
+
# so they can be selectively sub-classed where needed.
|
12
|
+
#
|
13
|
+
# The first type is a Query. The Query is an object describing
|
14
|
+
# the database-specific operations we wish to perform, in an
|
15
|
+
# abstract manner. For example: While most if not all databases
|
16
|
+
# support a mechanism for limiting the size of results returned,
|
17
|
+
# some use a "LIMIT" keyword, while others use a "TOP" keyword.
|
18
|
+
# We can set a SelectStatement#limit field then, and allow
|
19
|
+
# the adapter to override the underlying SQL generated.
|
20
|
+
# Refer to DataMapper::Queries.
|
21
|
+
#
|
22
|
+
# The final type provided is a DataMapper::Transaction.
|
23
|
+
# Transactions are duck-typed Connections that span multiple queries.
|
24
|
+
#
|
25
|
+
# Note: It is assumed that the Adapter implements it's own
|
26
|
+
# ConnectionPool if any since some libraries implement their own at
|
27
|
+
# a low-level, and it wouldn't make sense to pay a performance
|
28
|
+
# cost twice by implementing a secondary pool in the DataMapper itself.
|
29
|
+
# If the library being adapted does not provide such functionality,
|
30
|
+
# DataMapper::Support::ConnectionPool can be used.
|
31
|
+
module Adapters
|
32
|
+
|
33
|
+
# You must inherit from the DoAdapter, and implement the
|
34
|
+
# required methods to adapt a database library for use with the DataMapper.
|
35
|
+
#
|
36
|
+
# NOTE: By inheriting from DoAdapter, you get a copy of all the
|
37
|
+
# standard sub-modules (Quoting, Coersion and Queries) in your own Adapter.
|
38
|
+
# You can extend and overwrite these copies without affecting the originals.
|
39
|
+
class DataObjectAdapter < AbstractAdapter
|
40
|
+
|
41
|
+
$LOAD_PATH << (DM_PLUGINS_ROOT + '/dataobjects')
|
42
|
+
|
43
|
+
FIND_OPTIONS = [
|
44
|
+
:select, :offset, :limit, :class, :include, :shallow_include, :reload, :conditions, :order, :intercept_load
|
45
|
+
]
|
46
|
+
|
47
|
+
TABLE_QUOTING_CHARACTER = '`'.freeze
|
48
|
+
COLUMN_QUOTING_CHARACTER = '`'.freeze
|
49
|
+
|
50
|
+
def initialize(configuration)
|
51
|
+
super
|
52
|
+
|
53
|
+
unless @configuration.single_threaded?
|
54
|
+
@connection_pool = Support::ConnectionPool.new { create_connection }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_connection
|
59
|
+
raise NotImplementedError.new
|
60
|
+
end
|
61
|
+
|
62
|
+
# Yields an available connection. Flushes the connection-pool and reconnects
|
63
|
+
# if the connection returns an error.
|
64
|
+
def connection
|
65
|
+
begin
|
66
|
+
# Yield the appropriate connection
|
67
|
+
if @configuration.single_threaded?
|
68
|
+
yield(@active_connection || @active_connection = create_connection)
|
69
|
+
else
|
70
|
+
@connection_pool.hold { |active_connection| yield(active_connection) }
|
71
|
+
end
|
72
|
+
rescue => execution_error
|
73
|
+
# Log error on failure
|
74
|
+
@configuration.log.error(execution_error)
|
75
|
+
|
76
|
+
# Close all open connections, assuming that if one
|
77
|
+
# had an error, it's likely due to a lost connection,
|
78
|
+
# in which case all connections are likely broken.
|
79
|
+
begin
|
80
|
+
if @configuration.single_threaded?
|
81
|
+
@active_connection.close
|
82
|
+
else
|
83
|
+
@connection_pool.available_connections.each do |active_connection|
|
84
|
+
active_connection.close
|
85
|
+
end
|
86
|
+
end
|
87
|
+
rescue => close_connection_error
|
88
|
+
# An error on closing the connection is almost expected
|
89
|
+
# if the socket is broken.
|
90
|
+
@configuration.log.warn(close_connection_error)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Reopen fresh connections.
|
94
|
+
if @configuration.single_threaded?
|
95
|
+
@active_connection = create_connection
|
96
|
+
else
|
97
|
+
@connection_pool.available_connections.clear
|
98
|
+
end
|
99
|
+
|
100
|
+
raise execution_error
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def transaction(&block)
|
105
|
+
raise NotImplementedError.new
|
106
|
+
end
|
107
|
+
|
108
|
+
def execute(*args)
|
109
|
+
connection do |db|
|
110
|
+
sql = escape_sql(*args)
|
111
|
+
log.debug { sql }
|
112
|
+
result = nil
|
113
|
+
|
114
|
+
command = db.create_command(sql)
|
115
|
+
|
116
|
+
if block_given?
|
117
|
+
reader = command.execute_reader
|
118
|
+
result = yield(reader)
|
119
|
+
reader.close
|
120
|
+
else
|
121
|
+
result = command.execute_non_query
|
122
|
+
end
|
123
|
+
|
124
|
+
result
|
125
|
+
end
|
126
|
+
rescue => e
|
127
|
+
handle_error(e)
|
128
|
+
end
|
129
|
+
|
130
|
+
def query(*args)
|
131
|
+
execute(*args) do |reader|
|
132
|
+
fields = reader.fields.map { |field| Inflector.underscore(field).to_sym }
|
133
|
+
|
134
|
+
results = []
|
135
|
+
|
136
|
+
if fields.size > 1
|
137
|
+
struct = Struct.new(*fields)
|
138
|
+
|
139
|
+
reader.each do
|
140
|
+
results << struct.new(*reader.current_row)
|
141
|
+
end
|
142
|
+
else
|
143
|
+
reader.each do
|
144
|
+
results << reader.item(0)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
results
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def handle_error(error)
|
153
|
+
raise error
|
154
|
+
end
|
155
|
+
|
156
|
+
def schema
|
157
|
+
@schema || ( @schema = Mappings::Schema.new(self, @configuration.database) )
|
158
|
+
end
|
159
|
+
|
160
|
+
def table_exists?(name)
|
161
|
+
execute(table(name).to_exists_sql) { |reader| reader.has_rows? }
|
162
|
+
end
|
163
|
+
|
164
|
+
def truncate(session, name)
|
165
|
+
result = execute("TRUNCATE TABLE #{table(name).to_sql}")
|
166
|
+
session.identity_map.clear!(name)
|
167
|
+
result.to_i > 0
|
168
|
+
end
|
169
|
+
|
170
|
+
def drop(session, name)
|
171
|
+
result = execute("DROP TABLE #{table(name).to_sql}")
|
172
|
+
session.identity_map.clear!(name)
|
173
|
+
true
|
174
|
+
end
|
175
|
+
|
176
|
+
def create_table(name)
|
177
|
+
execute(table(name).to_create_table_sql); true
|
178
|
+
end
|
179
|
+
|
180
|
+
def delete(session, instance)
|
181
|
+
table = self.table(instance)
|
182
|
+
|
183
|
+
if instance.is_a?(Class)
|
184
|
+
execute("DELETE FROM #{table.to_sql}")
|
185
|
+
session.identity_map.clear!(instance)
|
186
|
+
else
|
187
|
+
callback(instance, :before_destroy)
|
188
|
+
|
189
|
+
if execute("DELETE FROM #{table.to_sql} WHERE #{table.key.to_sql} = #{quote_value(instance.key)}").to_i > 0
|
190
|
+
instance.instance_variable_set(:@new_record, true)
|
191
|
+
instance.session = session
|
192
|
+
instance.original_hashes.clear
|
193
|
+
session.identity_map.delete(instance)
|
194
|
+
callback(instance, :after_destroy)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def save(session, instance)
|
200
|
+
case instance
|
201
|
+
when Class, Mappings::Table then create_table(instance)
|
202
|
+
when DataMapper::Base then
|
203
|
+
return false unless instance.dirty? && instance.valid?
|
204
|
+
|
205
|
+
callback(instance, :before_save)
|
206
|
+
|
207
|
+
table = self.table(instance)
|
208
|
+
attributes = instance.dirty_attributes
|
209
|
+
|
210
|
+
unless attributes.empty?
|
211
|
+
attributes[:type] = instance.class.name if table.multi_class?
|
212
|
+
|
213
|
+
# INSERT
|
214
|
+
result = if instance.new_record?
|
215
|
+
callback(instance, :before_create)
|
216
|
+
|
217
|
+
keys = []
|
218
|
+
values = []
|
219
|
+
attributes.each_pair do |key, value|
|
220
|
+
keys << table[key].to_sql
|
221
|
+
values << value
|
222
|
+
end
|
223
|
+
|
224
|
+
# Formatting is a bit off here, but it looks nicer in the log this way.
|
225
|
+
insert_id = execute("INSERT INTO #{table.to_sql} (#{keys.join(', ')}) VALUES (#{values.map { |v| quote_value(v) }.join(', ')})").last_insert_row
|
226
|
+
instance.instance_variable_set(:@new_record, false)
|
227
|
+
instance.key = insert_id if table.key.serial? && !attributes.include?(table.key.name)
|
228
|
+
session.identity_map.set(instance)
|
229
|
+
callback(instance, :after_create)
|
230
|
+
# UPDATE
|
231
|
+
else
|
232
|
+
callback(instance, :before_update)
|
233
|
+
|
234
|
+
sql = "UPDATE " << table.to_sql << " SET "
|
235
|
+
|
236
|
+
sql << attributes.map do |key, value|
|
237
|
+
"#{table[key].to_sql} = #{quote_value(value)}"
|
238
|
+
end.join(', ')
|
239
|
+
|
240
|
+
sql << " WHERE #{table.key.to_sql} = " << quote_value(instance.key)
|
241
|
+
|
242
|
+
execute(sql).to_i > 0 && callback(instance, :after_update)
|
243
|
+
end
|
244
|
+
end # unless attributes.empty?
|
245
|
+
|
246
|
+
instance.attributes.each_pair do |name, value|
|
247
|
+
instance.original_hashes[name] = value.hash
|
248
|
+
end
|
249
|
+
|
250
|
+
instance.loaded_associations.each do |association|
|
251
|
+
association.save if association.respond_to?(:save)
|
252
|
+
end
|
253
|
+
|
254
|
+
instance.session = session
|
255
|
+
callback(instance, :after_save)
|
256
|
+
result
|
257
|
+
end
|
258
|
+
rescue => error
|
259
|
+
log.error(error)
|
260
|
+
raise error
|
261
|
+
end
|
262
|
+
|
263
|
+
def load(session, klass, options)
|
264
|
+
self.class::Commands::LoadCommand.new(self, session, klass, options).call
|
265
|
+
end
|
266
|
+
|
267
|
+
def count(klass_or_instance, options)
|
268
|
+
query("SELECT COUNT(*) AS row_count FROM " + table(klass_or_instance).to_sql).first.to_i
|
269
|
+
end
|
270
|
+
|
271
|
+
def table(instance)
|
272
|
+
case instance
|
273
|
+
when DataMapper::Adapters::Sql::Mappings::Table then instance
|
274
|
+
when DataMapper::Base then schema[instance.class]
|
275
|
+
when Class, String then schema[instance]
|
276
|
+
else raise "Don't know how to map #{instance.inspect} to a table."
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def callback(instance, callback_name)
|
281
|
+
instance.class.callbacks.execute(callback_name, instance)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Escape a string of SQL with a set of arguments.
|
285
|
+
# The first argument is assumed to be the SQL to escape,
|
286
|
+
# the remaining arguments (if any) are assumed to be
|
287
|
+
# values to escape and interpolate.
|
288
|
+
#
|
289
|
+
# ==== Examples
|
290
|
+
# escape_sql("SELECT * FROM zoos")
|
291
|
+
# # => "SELECT * FROM zoos"
|
292
|
+
#
|
293
|
+
# escape_sql("SELECT * FROM zoos WHERE name = ?", "Dallas")
|
294
|
+
# # => "SELECT * FROM zoos WHERE name = `Dallas`"
|
295
|
+
#
|
296
|
+
# escape_sql("SELECT * FROM zoos WHERE name = ? AND acreage > ?", "Dallas", 40)
|
297
|
+
# # => "SELECT * FROM zoos WHERE name = `Dallas` AND acreage > 40"
|
298
|
+
#
|
299
|
+
# ==== Warning
|
300
|
+
# This method is meant mostly for adapters that don't support
|
301
|
+
# bind-parameters.
|
302
|
+
def escape_sql(*args)
|
303
|
+
sql = args.shift
|
304
|
+
|
305
|
+
unless args.empty?
|
306
|
+
sql.gsub!(/\?/) do |x|
|
307
|
+
quote_value(args.shift)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
sql
|
312
|
+
end
|
313
|
+
|
314
|
+
# This callback copies and sub-classes modules and classes
|
315
|
+
# in the DoAdapter to the inherited class so you don't
|
316
|
+
# have to copy and paste large blocks of code from the
|
317
|
+
# DoAdapter.
|
318
|
+
#
|
319
|
+
# Basically, when inheriting from the DoAdapter, you
|
320
|
+
# aren't just inheriting a single class, you're inheriting
|
321
|
+
# a whole graph of Types. For convenience.
|
322
|
+
def self.inherited(base)
|
323
|
+
|
324
|
+
commands = base.const_set('Commands', Module.new)
|
325
|
+
|
326
|
+
Sql::Commands.constants.each do |name|
|
327
|
+
commands.const_set(name, Class.new(Sql::Commands.const_get(name)))
|
328
|
+
end
|
329
|
+
|
330
|
+
mappings = base.const_set('Mappings', Module.new)
|
331
|
+
|
332
|
+
Sql::Mappings.constants.each do |name|
|
333
|
+
mappings.const_set(name, Class.new(Sql::Mappings.const_get(name)))
|
334
|
+
end
|
335
|
+
|
336
|
+
base.const_set('TYPES', TYPES.dup)
|
337
|
+
base.const_set('FIND_OPTIONS', FIND_OPTIONS.dup)
|
338
|
+
|
339
|
+
super
|
340
|
+
end
|
341
|
+
|
342
|
+
TYPES = {
|
343
|
+
:integer => 'int'.freeze,
|
344
|
+
:string => 'varchar'.freeze,
|
345
|
+
:text => 'text'.freeze,
|
346
|
+
:class => 'varchar'.freeze,
|
347
|
+
:decimal => 'decimal'.freeze,
|
348
|
+
:float => 'float'.freeze,
|
349
|
+
:datetime => 'datetime'.freeze,
|
350
|
+
:date => 'date'.freeze
|
351
|
+
}
|
352
|
+
|
353
|
+
include Sql
|
354
|
+
include Quoting
|
355
|
+
include Coersion
|
356
|
+
|
357
|
+
end # class DoAdapter
|
358
|
+
|
359
|
+
end # module Adapters
|
360
|
+
end # module DataMapper
|