activegroonga 0.0.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/AUTHORS +1 -0
- data/NEWS.ja.rdoc +5 -0
- data/NEWS.rdoc +5 -0
- data/README.ja.rdoc +49 -0
- data/README.rdoc +49 -0
- data/Rakefile +175 -0
- data/lib/active_groonga.rb +75 -0
- data/lib/active_groonga/aggregations.rb +30 -0
- data/lib/active_groonga/associations.rb +93 -0
- data/lib/active_groonga/associations/belongs_to_association.rb +25 -0
- data/lib/active_groonga/attribute_methods.rb +36 -0
- data/lib/active_groonga/base.rb +1579 -0
- data/lib/active_groonga/column.rb +107 -0
- data/lib/active_groonga/dirty.rb +30 -0
- data/lib/active_groonga/fixtures.rb +92 -0
- data/lib/active_groonga/migration.rb +150 -0
- data/lib/active_groonga/rails_support.rb +31 -0
- data/lib/active_groonga/reflection.rb +30 -0
- data/lib/active_groonga/schema.rb +314 -0
- data/lib/active_groonga/schema_dumper.rb +147 -0
- data/lib/active_groonga/tasks.rb +16 -0
- data/lib/active_groonga/tasks/groonga.rake +162 -0
- data/lib/active_groonga/test_case.rb +21 -0
- data/lib/active_groonga/test_help.rb +21 -0
- data/lib/active_groonga/timestamp.rb +30 -0
- data/lib/active_groonga/validations.rb +26 -0
- data/lib/active_groonga/version.rb +24 -0
- data/license/LGPL +504 -0
- data/rails/README +28 -0
- data/rails/init.rb +70 -0
- data/rails_generators/model_groonga/USAGE +28 -0
- data/rails_generators/model_groonga/model_groonga_generator.rb +45 -0
- data/rails_generators/model_groonga/templates/fixtures.yml +17 -0
- data/rails_generators/model_groonga/templates/migration.rb +16 -0
- data/rails_generators/model_groonga/templates/model.rb +2 -0
- data/rails_generators/model_groonga/templates/unit_test.rb +8 -0
- data/test-unit/Rakefile +35 -0
- data/test-unit/TODO +5 -0
- data/test-unit/bin/testrb +5 -0
- data/test-unit/html/classic.html +15 -0
- data/test-unit/html/index.html +25 -0
- data/test-unit/html/index.html.ja +27 -0
- data/test-unit/lib/test/unit.rb +342 -0
- data/test-unit/lib/test/unit/assertionfailederror.rb +14 -0
- data/test-unit/lib/test/unit/assertions.rb +1149 -0
- data/test-unit/lib/test/unit/attribute.rb +125 -0
- data/test-unit/lib/test/unit/autorunner.rb +306 -0
- data/test-unit/lib/test/unit/collector.rb +43 -0
- data/test-unit/lib/test/unit/collector/descendant.rb +23 -0
- data/test-unit/lib/test/unit/collector/dir.rb +108 -0
- data/test-unit/lib/test/unit/collector/load.rb +135 -0
- data/test-unit/lib/test/unit/collector/objectspace.rb +34 -0
- data/test-unit/lib/test/unit/color-scheme.rb +86 -0
- data/test-unit/lib/test/unit/color.rb +96 -0
- data/test-unit/lib/test/unit/diff.rb +538 -0
- data/test-unit/lib/test/unit/error.rb +124 -0
- data/test-unit/lib/test/unit/exceptionhandler.rb +39 -0
- data/test-unit/lib/test/unit/failure.rb +110 -0
- data/test-unit/lib/test/unit/fixture.rb +176 -0
- data/test-unit/lib/test/unit/notification.rb +125 -0
- data/test-unit/lib/test/unit/omission.rb +143 -0
- data/test-unit/lib/test/unit/pending.rb +146 -0
- data/test-unit/lib/test/unit/priority.rb +161 -0
- data/test-unit/lib/test/unit/runner/console.rb +52 -0
- data/test-unit/lib/test/unit/runner/emacs.rb +8 -0
- data/test-unit/lib/test/unit/testcase.rb +360 -0
- data/test-unit/lib/test/unit/testresult.rb +89 -0
- data/test-unit/lib/test/unit/testsuite.rb +110 -0
- data/test-unit/lib/test/unit/ui/console/outputlevel.rb +14 -0
- data/test-unit/lib/test/unit/ui/console/testrunner.rb +220 -0
- data/test-unit/lib/test/unit/ui/emacs/testrunner.rb +49 -0
- data/test-unit/lib/test/unit/ui/testrunner.rb +20 -0
- data/test-unit/lib/test/unit/ui/testrunnermediator.rb +77 -0
- data/test-unit/lib/test/unit/ui/testrunnerutilities.rb +41 -0
- data/test-unit/lib/test/unit/util/backtracefilter.rb +41 -0
- data/test-unit/lib/test/unit/util/method-owner-finder.rb +28 -0
- data/test-unit/lib/test/unit/util/observable.rb +90 -0
- data/test-unit/lib/test/unit/util/procwrapper.rb +48 -0
- data/test-unit/lib/test/unit/version.rb +7 -0
- data/test-unit/sample/adder.rb +13 -0
- data/test-unit/sample/subtracter.rb +12 -0
- data/test-unit/sample/tc_adder.rb +18 -0
- data/test-unit/sample/tc_subtracter.rb +18 -0
- data/test-unit/sample/test_user.rb +22 -0
- data/test-unit/sample/ts_examples.rb +7 -0
- data/test-unit/test/collector/test-descendant.rb +135 -0
- data/test-unit/test/collector/test-load.rb +333 -0
- data/test-unit/test/collector/test_dir.rb +406 -0
- data/test-unit/test/collector/test_objectspace.rb +98 -0
- data/test-unit/test/run-test.rb +13 -0
- data/test-unit/test/test-attribute.rb +86 -0
- data/test-unit/test/test-color-scheme.rb +56 -0
- data/test-unit/test/test-color.rb +47 -0
- data/test-unit/test/test-diff.rb +477 -0
- data/test-unit/test/test-emacs-runner.rb +60 -0
- data/test-unit/test/test-fixture.rb +287 -0
- data/test-unit/test/test-notification.rb +33 -0
- data/test-unit/test/test-omission.rb +81 -0
- data/test-unit/test/test-pending.rb +70 -0
- data/test-unit/test/test-priority.rb +119 -0
- data/test-unit/test/test_assertions.rb +1082 -0
- data/test-unit/test/test_error.rb +26 -0
- data/test-unit/test/test_failure.rb +33 -0
- data/test-unit/test/test_testcase.rb +478 -0
- data/test-unit/test/test_testresult.rb +113 -0
- data/test-unit/test/test_testsuite.rb +129 -0
- data/test-unit/test/testunit-test-util.rb +14 -0
- data/test-unit/test/ui/test_testrunmediator.rb +20 -0
- data/test-unit/test/util/test-method-owner-finder.rb +38 -0
- data/test-unit/test/util/test_backtracefilter.rb +41 -0
- data/test-unit/test/util/test_observable.rb +102 -0
- data/test-unit/test/util/test_procwrapper.rb +36 -0
- data/test/active-groonga-test-utils.rb +234 -0
- data/test/fixtures/bookmark.rb +2 -0
- data/test/fixtures/task.rb +2 -0
- data/test/fixtures/user.rb +2 -0
- data/test/run-test.rb +51 -0
- data/test/test-associations.rb +24 -0
- data/test/test-base.rb +194 -0
- data/test/test-schema.rb +49 -0
- metadata +192 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
# Copyright (C) 2009 Kouhei Sutou <kou@clear-code.com>
|
2
|
+
#
|
3
|
+
# This library is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
5
|
+
# License version 2.1 as published by the Free Software Foundation.
|
6
|
+
#
|
7
|
+
# This library is distributed in the hope that it will be useful,
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
10
|
+
# Lesser General Public License for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU Lesser General Public
|
13
|
+
# License along with this library; if not, write to the Free Software
|
14
|
+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
15
|
+
|
16
|
+
module ActiveGroonga
|
17
|
+
module Associations
|
18
|
+
class BelongsToAssociation < ActiveRecord::Associations::BelongsToAssociation
|
19
|
+
def find_target
|
20
|
+
@reflection.klass.find(@owner.id,
|
21
|
+
:readonly => @reflection.options[:readonly])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Copyright (C) 2009 Kouhei Sutou <kou@clear-code.com>
|
2
|
+
#
|
3
|
+
# This library is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
5
|
+
# License version 2.1 as published by the Free Software Foundation.
|
6
|
+
#
|
7
|
+
# This library is distributed in the hope that it will be useful,
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
10
|
+
# Lesser General Public License for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU Lesser General Public
|
13
|
+
# License along with this library; if not, write to the Free Software
|
14
|
+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
15
|
+
|
16
|
+
module ActiveGroonga
|
17
|
+
module AttributeMethods
|
18
|
+
def self.included(base)
|
19
|
+
base.module_eval do
|
20
|
+
include ActiveRecord::AttributeMethods
|
21
|
+
extend AttributeMethods::ClassMethods
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
def instance_method_already_implemented?(method_name)
|
27
|
+
method_name = method_name.to_s
|
28
|
+
return true if method_name =~ /^id(=$|\?$|$)/
|
29
|
+
@_defined_class_methods ||= ancestors.first(ancestors.index(ActiveGroonga::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map(&:to_s).to_set
|
30
|
+
@@_defined_activegroonga_methods ||= (ActiveGroonga::Base.public_instance_methods(false) | ActiveGroonga::Base.private_instance_methods(false) | ActiveGroonga::Base.protected_instance_methods(false)).map(&:to_s).to_set
|
31
|
+
raise DangerousAttributeError, "#{method_name} is defined by ActiveGroonga" if @@_defined_activegroonga_methods.include?(method_name)
|
32
|
+
@_defined_class_methods.include?(method_name)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,1579 @@
|
|
1
|
+
# Copyright (C) 2009 Kouhei Sutou <kou@clear-code.com>
|
2
|
+
#
|
3
|
+
# This library is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
5
|
+
# License version 2.1 as published by the Free Software Foundation.
|
6
|
+
#
|
7
|
+
# This library is distributed in the hope that it will be useful,
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
10
|
+
# Lesser General Public License for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU Lesser General Public
|
13
|
+
# License along with this library; if not, write to the Free Software
|
14
|
+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
15
|
+
|
16
|
+
# This library includes ActiveRecord based codes temporary.
|
17
|
+
# Here is their copyright and license:
|
18
|
+
#
|
19
|
+
# Copyright (c) 2004-2009 David Heinemeier Hansson
|
20
|
+
#
|
21
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
22
|
+
# a copy of this software and associated documentation files (the
|
23
|
+
# "Software"), to deal in the Software without restriction, including
|
24
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
25
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
26
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
27
|
+
# the following conditions:
|
28
|
+
#
|
29
|
+
# The above copyright notice and this permission notice shall be
|
30
|
+
# included in all copies or substantial portions of the Software.
|
31
|
+
#
|
32
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
33
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
34
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
35
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
36
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
37
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
38
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
39
|
+
|
40
|
+
require 'active_record/base'
|
41
|
+
|
42
|
+
module ActiveGroonga
|
43
|
+
# Generic ActiveGroonga exception class.
|
44
|
+
class ActiveGroongaError < StandardError
|
45
|
+
end
|
46
|
+
|
47
|
+
# Raised when ActiveGroonga cannot find record by given id or set of ids.
|
48
|
+
class RecordNotFound < ActiveGroongaError
|
49
|
+
end
|
50
|
+
|
51
|
+
# Raised when database not specified (or configuration file <tt>config/groonga.yml</tt> misses database field).
|
52
|
+
class DatabaseNotSpecified < ActiveGroongaError
|
53
|
+
end
|
54
|
+
|
55
|
+
class Base
|
56
|
+
##
|
57
|
+
# :singleton-method:
|
58
|
+
# Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed
|
59
|
+
# on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+.
|
60
|
+
cattr_accessor :logger, :instance_writer => false
|
61
|
+
|
62
|
+
##
|
63
|
+
# :singleton-method:
|
64
|
+
# Contains the groonga configuration - as is typically stored in config/groonga.yml -
|
65
|
+
# as a Hash.
|
66
|
+
#
|
67
|
+
# For example, the following groonga.yml...
|
68
|
+
#
|
69
|
+
# development:
|
70
|
+
# database: db/development.groonga
|
71
|
+
#
|
72
|
+
# production:
|
73
|
+
# adapter: groonga
|
74
|
+
# database: db/production.groonga
|
75
|
+
#
|
76
|
+
# ...would result in ActiveGroonga::Base.configurations to look like this:
|
77
|
+
#
|
78
|
+
# {
|
79
|
+
# 'development' => {
|
80
|
+
# 'database' => 'db/development.groonga'
|
81
|
+
# },
|
82
|
+
# 'production' => {
|
83
|
+
# 'database' => 'db/production.groonga'
|
84
|
+
# }
|
85
|
+
# }
|
86
|
+
cattr_accessor :configurations, :instance_writer => false
|
87
|
+
@@configurations = {}
|
88
|
+
|
89
|
+
##
|
90
|
+
# :singleton-method:
|
91
|
+
# Accessor for the name of the prefix string to prepend to every table name. So if set to "basecamp_", all
|
92
|
+
# table names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient way of creating a namespace
|
93
|
+
# for tables in a shared database. By default, the prefix is the empty string.
|
94
|
+
cattr_accessor :table_name_prefix, :instance_writer => false
|
95
|
+
@@table_name_prefix = ""
|
96
|
+
|
97
|
+
##
|
98
|
+
# :singleton-method:
|
99
|
+
# Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
|
100
|
+
# "people_basecamp"). By default, the suffix is the empty string.
|
101
|
+
cattr_accessor :table_name_suffix, :instance_writer => false
|
102
|
+
@@table_name_suffix = ""
|
103
|
+
|
104
|
+
##
|
105
|
+
# :singleton-method:
|
106
|
+
# Indicates whether table names should be the pluralized versions of the corresponding class names.
|
107
|
+
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
|
108
|
+
# See table_name for the full rules on table/class naming. This is true, by default.
|
109
|
+
cattr_accessor :pluralize_table_names, :instance_writer => false
|
110
|
+
@@pluralize_table_names = true
|
111
|
+
|
112
|
+
##
|
113
|
+
# :singleton-method:
|
114
|
+
# Determines whether to use ANSI codes to colorize the logging statements committed by the connection adapter. These colors
|
115
|
+
# make it much easier to overview things during debugging (when used through a reader like +tail+ and on a black background), but
|
116
|
+
# may complicate matters if you use software like syslog. This is true, by default.
|
117
|
+
cattr_accessor :colorize_logging, :instance_writer => false
|
118
|
+
@@colorize_logging = true
|
119
|
+
|
120
|
+
##
|
121
|
+
# :singleton-method:
|
122
|
+
# Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates and times from the database.
|
123
|
+
# This is set to :local by default.
|
124
|
+
cattr_accessor :default_timezone, :instance_writer => false
|
125
|
+
@@default_timezone = :local
|
126
|
+
|
127
|
+
# Determine whether to store the full constant name including namespace when using STI
|
128
|
+
superclass_delegating_accessor :store_full_sti_class
|
129
|
+
self.store_full_sti_class = false
|
130
|
+
|
131
|
+
# Stores the default scope for the class
|
132
|
+
class_inheritable_accessor :default_scoping, :instance_writer => false
|
133
|
+
self.default_scoping = []
|
134
|
+
|
135
|
+
##
|
136
|
+
# :singleton-method:
|
137
|
+
# Specifies the format to use when dumping the database schema with Rails'
|
138
|
+
# Rakefile. If :sql, the schema is dumped as (potentially database-
|
139
|
+
# specific) SQL statements. If :ruby, the schema is dumped as an
|
140
|
+
# ActiveRecord::Schema file which can be loaded into any database that
|
141
|
+
# supports migrations. Use :ruby if you want to have different database
|
142
|
+
# adapters for, e.g., your development and test environments.
|
143
|
+
cattr_accessor :schema_format , :instance_writer => false
|
144
|
+
@@schema_format = :ruby
|
145
|
+
|
146
|
+
cattr_accessor :database_directory, :instance_writer => false
|
147
|
+
@@database_directory = nil
|
148
|
+
|
149
|
+
class << self
|
150
|
+
# Creates an object (or multiple objects) and saves it to the database, if validations pass.
|
151
|
+
# The resulting object is returned whether the object was saved successfully to the database or not.
|
152
|
+
#
|
153
|
+
# The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the
|
154
|
+
# attributes on the objects that are to be created.
|
155
|
+
#
|
156
|
+
# ==== Examples
|
157
|
+
# # Create a single new object
|
158
|
+
# User.create(:first_name => 'Jamie')
|
159
|
+
#
|
160
|
+
# # Create an Array of new objects
|
161
|
+
# User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
|
162
|
+
#
|
163
|
+
# # Create a single object and pass it into a block to set other attributes.
|
164
|
+
# User.create(:first_name => 'Jamie') do |u|
|
165
|
+
# u.is_admin = false
|
166
|
+
# end
|
167
|
+
#
|
168
|
+
# # Creating an Array of new objects using a block, where the block is executed for each object:
|
169
|
+
# User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
|
170
|
+
# u.is_admin = false
|
171
|
+
# end
|
172
|
+
def create(attributes = nil, &block)
|
173
|
+
if attributes.is_a?(Array)
|
174
|
+
attributes.collect { |attr| create(attr, &block) }
|
175
|
+
else
|
176
|
+
object = new(attributes)
|
177
|
+
yield(object) if block_given?
|
178
|
+
object.save
|
179
|
+
object
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Attributes named in this macro are protected from mass-assignment,
|
184
|
+
# such as <tt>new(attributes)</tt>,
|
185
|
+
# <tt>update_attributes(attributes)</tt>, or
|
186
|
+
# <tt>attributes=(attributes)</tt>.
|
187
|
+
#
|
188
|
+
# Mass-assignment to these attributes will simply be ignored, to assign
|
189
|
+
# to them you can use direct writer methods. This is meant to protect
|
190
|
+
# sensitive attributes from being overwritten by malicious users
|
191
|
+
# tampering with URLs or forms.
|
192
|
+
#
|
193
|
+
# class Customer < ActiveRecord::Base
|
194
|
+
# attr_protected :credit_rating
|
195
|
+
# end
|
196
|
+
#
|
197
|
+
# customer = Customer.new("name" => David, "credit_rating" => "Excellent")
|
198
|
+
# customer.credit_rating # => nil
|
199
|
+
# customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" }
|
200
|
+
# customer.credit_rating # => nil
|
201
|
+
#
|
202
|
+
# customer.credit_rating = "Average"
|
203
|
+
# customer.credit_rating # => "Average"
|
204
|
+
#
|
205
|
+
# To start from an all-closed default and enable attributes as needed,
|
206
|
+
# have a look at +attr_accessible+.
|
207
|
+
def attr_protected(*attributes)
|
208
|
+
write_inheritable_attribute(:attr_protected, Set.new(attributes.map(&:to_s)) + (protected_attributes || []))
|
209
|
+
end
|
210
|
+
|
211
|
+
# Returns an array of all the attributes that have been protected from mass-assignment.
|
212
|
+
def protected_attributes # :nodoc:
|
213
|
+
read_inheritable_attribute(:attr_protected)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Specifies a white list of model attributes that can be set via
|
217
|
+
# mass-assignment, such as <tt>new(attributes)</tt>,
|
218
|
+
# <tt>update_attributes(attributes)</tt>, or
|
219
|
+
# <tt>attributes=(attributes)</tt>
|
220
|
+
#
|
221
|
+
# This is the opposite of the +attr_protected+ macro: Mass-assignment
|
222
|
+
# will only set attributes in this list, to assign to the rest of
|
223
|
+
# attributes you can use direct writer methods. This is meant to protect
|
224
|
+
# sensitive attributes from being overwritten by malicious users
|
225
|
+
# tampering with URLs or forms. If you'd rather start from an all-open
|
226
|
+
# default and restrict attributes as needed, have a look at
|
227
|
+
# +attr_protected+.
|
228
|
+
#
|
229
|
+
# class Customer < ActiveRecord::Base
|
230
|
+
# attr_accessible :name, :nickname
|
231
|
+
# end
|
232
|
+
#
|
233
|
+
# customer = Customer.new(:name => "David", :nickname => "Dave", :credit_rating => "Excellent")
|
234
|
+
# customer.credit_rating # => nil
|
235
|
+
# customer.attributes = { :name => "Jolly fellow", :credit_rating => "Superb" }
|
236
|
+
# customer.credit_rating # => nil
|
237
|
+
#
|
238
|
+
# customer.credit_rating = "Average"
|
239
|
+
# customer.credit_rating # => "Average"
|
240
|
+
def attr_accessible(*attributes)
|
241
|
+
write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns an array of all the attributes that have been made accessible to mass-assignment.
|
245
|
+
def accessible_attributes # :nodoc:
|
246
|
+
read_inheritable_attribute(:attr_accessible)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards.
|
250
|
+
def attr_readonly(*attributes)
|
251
|
+
write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || []))
|
252
|
+
end
|
253
|
+
|
254
|
+
# Returns an array of all the attributes that have been specified as readonly.
|
255
|
+
def readonly_attributes
|
256
|
+
read_inheritable_attribute(:attr_readonly)
|
257
|
+
end
|
258
|
+
|
259
|
+
|
260
|
+
# If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
|
261
|
+
# then specify the name of that attribute using this method and it will be handled automatically.
|
262
|
+
# The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that
|
263
|
+
# class on retrieval or SerializationTypeMismatch will be raised.
|
264
|
+
#
|
265
|
+
# ==== Parameters
|
266
|
+
#
|
267
|
+
# * +attr_name+ - The field name that should be serialized.
|
268
|
+
# * +class_name+ - Optional, class name that the object type should be equal to.
|
269
|
+
#
|
270
|
+
# ==== Example
|
271
|
+
# # Serialize a preferences attribute
|
272
|
+
# class User
|
273
|
+
# serialize :preferences
|
274
|
+
# end
|
275
|
+
def serialize(attr_name, class_name = Object)
|
276
|
+
serialized_attributes[attr_name.to_s] = class_name
|
277
|
+
end
|
278
|
+
|
279
|
+
# Returns a hash of all the attributes that have been specified for serialization as keys and their class restriction as values.
|
280
|
+
def serialized_attributes
|
281
|
+
read_inheritable_attribute(:attr_serialized) or write_inheritable_attribute(:attr_serialized, {})
|
282
|
+
end
|
283
|
+
|
284
|
+
# Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
|
285
|
+
# directly from ActiveGroonga::Base. So if the hierarchy looks like: Reply < Message < ActiveGroonga::Base, then Message is used
|
286
|
+
# to guess the table name even when called on Reply. The rules used to do the guess are handled by the Inflector class
|
287
|
+
# in Active Support, which knows almost all common English inflections. You can add new inflections in config/initializers/inflections.rb.
|
288
|
+
#
|
289
|
+
# Nested classes are given table names prefixed by the singular form of
|
290
|
+
# the parent's table name. Enclosing modules are not considered.
|
291
|
+
#
|
292
|
+
# ==== Examples
|
293
|
+
#
|
294
|
+
# class Invoice < ActiveGroonga::Base; end;
|
295
|
+
# file class table_name
|
296
|
+
# invoice.rb Invoice invoices
|
297
|
+
#
|
298
|
+
# class Invoice < ActiveGroonga::Base; class Lineitem < ActiveGroonga::Base; end; end;
|
299
|
+
# file class table_name
|
300
|
+
# invoice.rb Invoice::Lineitem invoice_lineitems
|
301
|
+
#
|
302
|
+
# module Invoice; class Lineitem < ActiveGroonga::Base; end; end;
|
303
|
+
# file class table_name
|
304
|
+
# invoice/lineitem.rb Invoice::Lineitem lineitems
|
305
|
+
#
|
306
|
+
# Additionally, the class-level +table_name_prefix+ is prepended and the
|
307
|
+
# +table_name_suffix+ is appended. So if you have "myapp_" as a prefix,
|
308
|
+
# the table name guess for an Invoice class becomes "myapp_invoices".
|
309
|
+
# Invoice::Lineitem becomes "myapp_invoice_lineitems".
|
310
|
+
#
|
311
|
+
# You can also overwrite this class method to allow for unguessable
|
312
|
+
# links, such as a Mouse class with a link to a "mice" table. Example:
|
313
|
+
#
|
314
|
+
# class Mouse < ActiveGroonga::Base
|
315
|
+
# set_table_name "mice"
|
316
|
+
# end
|
317
|
+
def table_name
|
318
|
+
reset_table_name
|
319
|
+
end
|
320
|
+
|
321
|
+
def reset_table_name #:nodoc:
|
322
|
+
base = base_class
|
323
|
+
|
324
|
+
name =
|
325
|
+
# STI subclasses always use their superclass' table.
|
326
|
+
unless self == base
|
327
|
+
base.table_name
|
328
|
+
else
|
329
|
+
# Nested classes are prefixed with singular parent table name.
|
330
|
+
if parent < ActiveGroonga::Base && !parent.abstract_class?
|
331
|
+
contained = parent.table_name
|
332
|
+
contained = contained.singularize if parent.pluralize_table_names
|
333
|
+
contained << '_'
|
334
|
+
end
|
335
|
+
name = "#{table_name_prefix}#{contained}#{undecorated_table_name(base.name)}#{table_name_suffix}"
|
336
|
+
end
|
337
|
+
|
338
|
+
set_table_name(name)
|
339
|
+
name
|
340
|
+
end
|
341
|
+
|
342
|
+
# Defines the column name for use with single table inheritance
|
343
|
+
# -- can be set in subclasses like so: self.inheritance_column = "type_id"
|
344
|
+
def inheritance_column
|
345
|
+
@inheritance_column ||= "type".freeze
|
346
|
+
end
|
347
|
+
|
348
|
+
# Sets the table name to use to the given value, or (if the value
|
349
|
+
# is nil or false) to the value returned by the given block.
|
350
|
+
#
|
351
|
+
# class Project < ActiveGroonga::Base
|
352
|
+
# set_table_name "project"
|
353
|
+
# end
|
354
|
+
def set_table_name(value = nil, &block)
|
355
|
+
define_attr_method :table_name, value, &block
|
356
|
+
end
|
357
|
+
alias :table_name= :set_table_name
|
358
|
+
|
359
|
+
# Turns the +table_name+ back into a class name following the reverse rules of +table_name+.
|
360
|
+
def class_name(table_name = table_name) # :nodoc:
|
361
|
+
# remove any prefix and/or suffix from the table name
|
362
|
+
class_name = table_name[table_name_prefix.length..-(table_name_suffix.length + 1)].camelize
|
363
|
+
class_name = class_name.singularize if pluralize_table_names
|
364
|
+
class_name
|
365
|
+
end
|
366
|
+
|
367
|
+
# Indicates whether the table associated with this class exists
|
368
|
+
def table_exists?
|
369
|
+
not table.nil?
|
370
|
+
end
|
371
|
+
|
372
|
+
def primary_key
|
373
|
+
"id"
|
374
|
+
end
|
375
|
+
|
376
|
+
# Returns an array of column objects for the table associated with this class.
|
377
|
+
def columns
|
378
|
+
@columns ||= table.columns.collect do |column|
|
379
|
+
Column.new(column)
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
# Returns a hash of column objects for the table associated with this class.
|
384
|
+
def columns_hash
|
385
|
+
@columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash }
|
386
|
+
end
|
387
|
+
|
388
|
+
# Returns an array of column names as strings.
|
389
|
+
def column_names
|
390
|
+
@column_names ||= columns.map { |column| column.name }
|
391
|
+
end
|
392
|
+
|
393
|
+
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
|
394
|
+
# and columns used for single table inheritance have been removed.
|
395
|
+
def content_columns
|
396
|
+
@content_columns ||= columns.reject do |c|
|
397
|
+
c.primary || c.type == :references || c.name == inheritance_column
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
|
402
|
+
# and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
|
403
|
+
# is available.
|
404
|
+
def column_methods_hash #:nodoc:
|
405
|
+
@dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr|
|
406
|
+
attr_name = attr.to_s
|
407
|
+
methods[attr.to_sym] = attr_name
|
408
|
+
methods["#{attr}=".to_sym] = attr_name
|
409
|
+
methods["#{attr}?".to_sym] = attr_name
|
410
|
+
methods["#{attr}_before_type_cast".to_sym] = attr_name
|
411
|
+
methods
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# True if this isn't a concrete subclass needing a STI type condition.
|
416
|
+
def descends_from_active_groonga?
|
417
|
+
if superclass.abstract_class?
|
418
|
+
superclass.descends_from_active_groonga?
|
419
|
+
else
|
420
|
+
superclass == Base || !columns_hash.include?(inheritance_column)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
# Returns a string like 'Post id:integer, title:string, body:text'
|
425
|
+
def inspect
|
426
|
+
if self == Base
|
427
|
+
super
|
428
|
+
elsif abstract_class?
|
429
|
+
"#{super}(abstract)"
|
430
|
+
elsif table_exists?
|
431
|
+
attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
|
432
|
+
"#{super}(#{attr_list})"
|
433
|
+
else
|
434
|
+
"#{super}(Table doesn't exist)"
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
# Log and benchmark multiple statements in a single block. Example:
|
439
|
+
#
|
440
|
+
# Project.benchmark("Creating project") do
|
441
|
+
# project = Project.create("name" => "stuff")
|
442
|
+
# project.create_manager("name" => "David")
|
443
|
+
# project.milestones << Milestone.find(:all)
|
444
|
+
# end
|
445
|
+
#
|
446
|
+
# The benchmark is only recorded if the current level of the logger is less than or equal to the <tt>log_level</tt>,
|
447
|
+
# which makes it easy to include benchmarking statements in production software that will remain inexpensive because
|
448
|
+
# the benchmark will only be conducted if the log level is low enough.
|
449
|
+
#
|
450
|
+
# The logging of the multiple statements is turned off unless <tt>use_silence</tt> is set to false.
|
451
|
+
def benchmark(title, log_level=Logger::DEBUG, use_silence=true)
|
452
|
+
if logger && logger.level <= log_level
|
453
|
+
result = nil
|
454
|
+
ms = Benchmark.ms { result = use_silence ? silence { yield } : yield }
|
455
|
+
logger.add(log_level, '%s (%.1fms)' % [title, ms])
|
456
|
+
result
|
457
|
+
else
|
458
|
+
yield
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
# Overwrite the default class equality method to provide support for association proxies.
|
463
|
+
def ===(object)
|
464
|
+
object.is_a?(self)
|
465
|
+
end
|
466
|
+
|
467
|
+
# Returns the base AR subclass that this class descends from. If A
|
468
|
+
# extends AR::Base, A.base_class will return A. If B descends from A
|
469
|
+
# through some arbitrarily deep hierarchy, B.base_class will return A.
|
470
|
+
def base_class
|
471
|
+
class_of_active_groonga_descendant(self)
|
472
|
+
end
|
473
|
+
|
474
|
+
# Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
|
475
|
+
attr_accessor :abstract_class
|
476
|
+
|
477
|
+
# Returns whether this class is a base AR class. If A is a base class and
|
478
|
+
# B descends from A, then B.base_class will return B.
|
479
|
+
def abstract_class?
|
480
|
+
defined?(@abstract_class) && @abstract_class == true
|
481
|
+
end
|
482
|
+
|
483
|
+
def find(*args)
|
484
|
+
options = args.extract_options!
|
485
|
+
validate_find_options(options)
|
486
|
+
set_readonly_option!(options)
|
487
|
+
|
488
|
+
case args.first
|
489
|
+
when :first
|
490
|
+
find_initial(options)
|
491
|
+
when :last
|
492
|
+
find_last(options)
|
493
|
+
when :all
|
494
|
+
find_every(options)
|
495
|
+
else
|
496
|
+
find_from_ids(args, options)
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
# A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
|
501
|
+
# same arguments to this method as you can to <tt>find(:first)</tt>.
|
502
|
+
def first(*args)
|
503
|
+
find(:first, *args)
|
504
|
+
end
|
505
|
+
|
506
|
+
# A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the
|
507
|
+
# same arguments to this method as you can to <tt>find(:last)</tt>.
|
508
|
+
def last(*args)
|
509
|
+
find(:last, *args)
|
510
|
+
end
|
511
|
+
|
512
|
+
# This is an alias for find(:all). You can pass in all the same arguments to this method as you can
|
513
|
+
# to find(:all)
|
514
|
+
def all(*args)
|
515
|
+
find(:all, *args)
|
516
|
+
end
|
517
|
+
|
518
|
+
def context
|
519
|
+
Groonga::Context.default
|
520
|
+
end
|
521
|
+
|
522
|
+
def database
|
523
|
+
context.database
|
524
|
+
end
|
525
|
+
|
526
|
+
def table
|
527
|
+
context[groonga_table_name]
|
528
|
+
end
|
529
|
+
|
530
|
+
def groonga_table_name(name=nil)
|
531
|
+
"<table:#{name || table_name}>"
|
532
|
+
end
|
533
|
+
|
534
|
+
def groonga_metadata_table_name(name)
|
535
|
+
"<metadata:#{name}>"
|
536
|
+
end
|
537
|
+
|
538
|
+
# Defines an "attribute" method (like +inheritance_column+ or
|
539
|
+
# +table_name+). A new (class) method will be created with the
|
540
|
+
# given name. If a value is specified, the new method will
|
541
|
+
# return that value (as a string). Otherwise, the given block
|
542
|
+
# will be used to compute the value of the method.
|
543
|
+
#
|
544
|
+
# The original method will be aliased, with the new name being
|
545
|
+
# prefixed with "original_". This allows the new method to
|
546
|
+
# access the original value.
|
547
|
+
#
|
548
|
+
# Example:
|
549
|
+
#
|
550
|
+
# class A < ActiveRecord::Base
|
551
|
+
# define_attr_method :primary_key, "sysid"
|
552
|
+
# define_attr_method( :inheritance_column ) do
|
553
|
+
# original_inheritance_column + "_id"
|
554
|
+
# end
|
555
|
+
# end
|
556
|
+
def define_attr_method(name, value=nil, &block)
|
557
|
+
sing = class << self; self; end
|
558
|
+
sing.send :alias_method, "original_#{name}", name
|
559
|
+
if block_given?
|
560
|
+
sing.send :define_method, name, &block
|
561
|
+
else
|
562
|
+
# use eval instead of a block to work around a memory leak in dev
|
563
|
+
# mode in fcgi
|
564
|
+
sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
def setup_database(spec=nil)
|
569
|
+
case spec
|
570
|
+
when nil
|
571
|
+
raise DatabaseNotSpecified unless defined? RAILS_ENV
|
572
|
+
setup_database(RAILS_ENV)
|
573
|
+
when Symbol, String
|
574
|
+
if configuration = configurations[spec.to_s]
|
575
|
+
setup_database(configuration)
|
576
|
+
else
|
577
|
+
raise DatabaseNotSpecified, "#{spec} database is not configured"
|
578
|
+
end
|
579
|
+
else
|
580
|
+
spec = spec.symbolize_keys
|
581
|
+
unless spec.key?(:database)
|
582
|
+
raise DatabaseNotSpecified, "groonga configuration does not specify database"
|
583
|
+
end
|
584
|
+
database_directory = spec[:database]
|
585
|
+
|
586
|
+
Groonga::Context.default = nil
|
587
|
+
Groonga::Context.default_options = {:encoding => spec[:encoding]}
|
588
|
+
unless File.exist?(database_directory)
|
589
|
+
FileUtils.mkdir_p(database_directory)
|
590
|
+
end
|
591
|
+
database_file = File.join(database_directory, "database.groonga")
|
592
|
+
if File.exist?(database_file)
|
593
|
+
Groonga::Database.new(database_file)
|
594
|
+
else
|
595
|
+
Groonga::Database.create(:path => database_file)
|
596
|
+
end
|
597
|
+
self.database_directory = database_directory
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
def tables_directory
|
602
|
+
directory = File.join(database_directory, "tables")
|
603
|
+
FileUtils.mkdir_p(directory) unless File.exist?(directory)
|
604
|
+
directory
|
605
|
+
end
|
606
|
+
|
607
|
+
def columns_directory(table_name)
|
608
|
+
directory = File.join(tables_directory, table_name.to_s, "columns")
|
609
|
+
FileUtils.mkdir_p(directory) unless File.exist?(directory)
|
610
|
+
directory
|
611
|
+
end
|
612
|
+
|
613
|
+
def metadata_directory
|
614
|
+
directory = File.join(database_directory, "metadata")
|
615
|
+
FileUtils.mkdir_p(directory) unless File.exist?(directory)
|
616
|
+
directory
|
617
|
+
end
|
618
|
+
|
619
|
+
def count
|
620
|
+
table.size
|
621
|
+
end
|
622
|
+
|
623
|
+
private
|
624
|
+
def find_initial(options)
|
625
|
+
options.update(:limit => 1)
|
626
|
+
find_every(options).first
|
627
|
+
end
|
628
|
+
|
629
|
+
def find_every(options)
|
630
|
+
limit = options[:limit] ||= 0
|
631
|
+
conditions = (options[:conditions] || {}).stringify_keys
|
632
|
+
include_associations = merge_includes(scope(:find, :include), options[:include])
|
633
|
+
|
634
|
+
if include_associations.any? && references_eager_loaded_tables?(options)
|
635
|
+
records = find_with_associations(options)
|
636
|
+
else
|
637
|
+
records = []
|
638
|
+
target_records = []
|
639
|
+
original_table = table
|
640
|
+
index_records = nil
|
641
|
+
Schema.indexes(table_name).each do |index_definition|
|
642
|
+
if conditions.has_key?(index_definition.column)
|
643
|
+
index_column_name =
|
644
|
+
"#{index_definition.table}/#{index_definition.column}"
|
645
|
+
index = Schema.index_table.column(index_column_name)
|
646
|
+
key = conditions.delete(index_definition.column)
|
647
|
+
index_records = index.search(key, :result => index_records)
|
648
|
+
end
|
649
|
+
end
|
650
|
+
if index_records
|
651
|
+
sorted_records = index_records.sort([".:score"], :limit => limit)
|
652
|
+
limit = sorted_records.size
|
653
|
+
target_records = sorted_records.records(:order => :ascending).collect do |record|
|
654
|
+
index_record_id = record.value.unpack("i")[0]
|
655
|
+
index_record = Groonga::Record.new(index_records, index_record_id)
|
656
|
+
target_record = index_record.key
|
657
|
+
target_record.instance_variable_set("@score", index_record.score)
|
658
|
+
def target_record.score
|
659
|
+
@score
|
660
|
+
end
|
661
|
+
target_record
|
662
|
+
end
|
663
|
+
else
|
664
|
+
target_records = original_table.records
|
665
|
+
limit = target_records.size if limit.zero?
|
666
|
+
end
|
667
|
+
target_records.each_with_index do |record, i|
|
668
|
+
break if records.size >= limit
|
669
|
+
unless conditions.all? do |name, value|
|
670
|
+
record[name] == value or
|
671
|
+
(record.reference_column?(name) and record[name].id == value)
|
672
|
+
end
|
673
|
+
next
|
674
|
+
end
|
675
|
+
records << instantiate(record)
|
676
|
+
end
|
677
|
+
if include_associations.any?
|
678
|
+
preload_associations(records, include_associations)
|
679
|
+
end
|
680
|
+
end
|
681
|
+
|
682
|
+
records.each {|record| record.readonly!} if options[:readonly]
|
683
|
+
|
684
|
+
records
|
685
|
+
end
|
686
|
+
|
687
|
+
def find_from_ids(ids, options)
|
688
|
+
expects_array = ids.first.kind_of?(Array)
|
689
|
+
return ids.first if expects_array && ids.first.empty?
|
690
|
+
|
691
|
+
ids = ids.flatten.compact.uniq
|
692
|
+
|
693
|
+
case ids.size
|
694
|
+
when 0
|
695
|
+
raise RecordNotFound, "Couldn't find #{name} without an ID"
|
696
|
+
when 1
|
697
|
+
result = find_one(ids.first, options)
|
698
|
+
expects_array ? [result] : result
|
699
|
+
else
|
700
|
+
find_some(ids, options)
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
704
|
+
def find_one(id, options)
|
705
|
+
if id.is_a?(Groonga::Record)
|
706
|
+
record = id
|
707
|
+
else
|
708
|
+
if id.is_a?(ActiveGroonga::Base)
|
709
|
+
id = id.id
|
710
|
+
else
|
711
|
+
id = Integer(id)
|
712
|
+
end
|
713
|
+
record = Groonga::Record.new(table, id)
|
714
|
+
end
|
715
|
+
result = instantiate(record)
|
716
|
+
if result.nil?
|
717
|
+
raise RecordNotFound, "Couldn't find #{name} with ID=#{id}"
|
718
|
+
end
|
719
|
+
result
|
720
|
+
end
|
721
|
+
|
722
|
+
def find_some(ids, options)
|
723
|
+
result = ids.collect do |id|
|
724
|
+
context[id]
|
725
|
+
end
|
726
|
+
n_not_found_ids = result.count(nil)
|
727
|
+
if n_not_found_ids.zero?
|
728
|
+
result
|
729
|
+
else
|
730
|
+
raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids}) (found #{result.compact.size} results, but was looking for #{ids.size})"
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
def merge_includes(first, second)
|
735
|
+
(safe_to_array(first) + safe_to_array(second)).uniq
|
736
|
+
end
|
737
|
+
|
738
|
+
# ugly. derived from Active Record. FIXME: remove it.
|
739
|
+
def safe_to_array(o)
|
740
|
+
case o
|
741
|
+
when NilClass
|
742
|
+
[]
|
743
|
+
when Array
|
744
|
+
o
|
745
|
+
else
|
746
|
+
[o]
|
747
|
+
end
|
748
|
+
end
|
749
|
+
|
750
|
+
VALID_FIND_OPTIONS = [:conditions, :readonly, :limit]
|
751
|
+
def validate_find_options(options)
|
752
|
+
options.assert_valid_keys(VALID_FIND_OPTIONS)
|
753
|
+
end
|
754
|
+
|
755
|
+
def set_readonly_option!(options) #:nodoc:
|
756
|
+
# Inherit :readonly from finder scope if set. Otherwise,
|
757
|
+
# if :joins is not blank then :readonly defaults to true.
|
758
|
+
unless options.has_key?(:readonly)
|
759
|
+
if scoped_readonly = scope(:find, :readonly)
|
760
|
+
options[:readonly] = scoped_readonly
|
761
|
+
elsif !options[:joins].blank? && !options[:select]
|
762
|
+
options[:readonly] = true
|
763
|
+
end
|
764
|
+
end
|
765
|
+
end
|
766
|
+
|
767
|
+
# Guesses the table name, but does not decorate it with prefix and suffix information.
|
768
|
+
def undecorated_table_name(class_name = base_class.name)
|
769
|
+
table_name = class_name.to_s.demodulize.underscore
|
770
|
+
table_name = table_name.pluralize if pluralize_table_names
|
771
|
+
table_name
|
772
|
+
end
|
773
|
+
|
774
|
+
# Finder methods must instantiate through this method to work with the
|
775
|
+
# single-table inheritance model that makes it possible to create
|
776
|
+
# objects of different types from the same table.
|
777
|
+
def instantiate(record)
|
778
|
+
object =
|
779
|
+
if subclass_name = record[inheritance_column]
|
780
|
+
# No type given.
|
781
|
+
if subclass_name.empty?
|
782
|
+
allocate
|
783
|
+
|
784
|
+
else
|
785
|
+
# Ignore type if no column is present since it was probably
|
786
|
+
# pulled in from a sloppy join.
|
787
|
+
unless columns_hash.include?(inheritance_column)
|
788
|
+
allocate
|
789
|
+
|
790
|
+
else
|
791
|
+
begin
|
792
|
+
compute_type(subclass_name).allocate
|
793
|
+
rescue NameError
|
794
|
+
raise SubclassNotFound,
|
795
|
+
"The single-table inheritance mechanism failed to locate the subclass: '#{record[inheritance_column]}'. " +
|
796
|
+
"This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
|
797
|
+
"Please rename this column if you didn't intend it to be used for storing the inheritance class " +
|
798
|
+
"or overwrite #{self.to_s}.inheritance_column to use another column for that information."
|
799
|
+
end
|
800
|
+
end
|
801
|
+
end
|
802
|
+
else
|
803
|
+
allocate
|
804
|
+
end
|
805
|
+
|
806
|
+
object.instance_variable_set("@id", record.id)
|
807
|
+
object.instance_variable_set("@score", record.score)
|
808
|
+
attributes = {}
|
809
|
+
record.table.columns.each do |column|
|
810
|
+
_, column_name = column.name.split(/\A#{record.table.name}\./, 2)
|
811
|
+
attributes[column_name] = column[record.id]
|
812
|
+
end
|
813
|
+
object.instance_variable_set("@attributes", attributes)
|
814
|
+
object.instance_variable_set("@attributes_cache", Hash.new)
|
815
|
+
|
816
|
+
if object.respond_to_without_attributes?(:after_find)
|
817
|
+
object.send(:callback, :after_find)
|
818
|
+
end
|
819
|
+
|
820
|
+
if object.respond_to_without_attributes?(:after_initialize)
|
821
|
+
object.send(:callback, :after_initialize)
|
822
|
+
end
|
823
|
+
|
824
|
+
object
|
825
|
+
end
|
826
|
+
|
827
|
+
# Enables dynamic finders like <tt>find_by_user_name(user_name)</tt> and <tt>find_by_user_name_and_password(user_name, password)</tt>
|
828
|
+
# that are turned into <tt>find(:first, :conditions => ["user_name = ?", user_name])</tt> and
|
829
|
+
# <tt>find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])</tt> respectively. Also works for
|
830
|
+
# <tt>find(:all)</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>find(:all, :conditions => ["amount = ?", 50])</tt>.
|
831
|
+
#
|
832
|
+
# It's even possible to use all the additional parameters to +find+. For example, the full interface for +find_all_by_amount+
|
833
|
+
# is actually <tt>find_all_by_amount(amount, options)</tt>.
|
834
|
+
#
|
835
|
+
# Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
|
836
|
+
# are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
|
837
|
+
# respectively.
|
838
|
+
#
|
839
|
+
# Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future
|
840
|
+
# attempts to use it do not run through method_missing.
|
841
|
+
def method_missing(method_id, *arguments, &block)
|
842
|
+
if match = ActiveRecord::DynamicFinderMatch.match(method_id)
|
843
|
+
attribute_names = match.attribute_names
|
844
|
+
super unless all_attributes_exists?(attribute_names)
|
845
|
+
if match.finder?
|
846
|
+
finder = match.finder
|
847
|
+
bang = match.bang?
|
848
|
+
# def self.find_by_login_and_activated(*args)
|
849
|
+
# options = args.extract_options!
|
850
|
+
# attributes = construct_attributes_from_arguments(
|
851
|
+
# [:login,:activated],
|
852
|
+
# args
|
853
|
+
# )
|
854
|
+
# finder_options = { :conditions => attributes }
|
855
|
+
# validate_find_options(options)
|
856
|
+
# set_readonly_option!(options)
|
857
|
+
#
|
858
|
+
# if options[:conditions]
|
859
|
+
# with_scope(:find => finder_options) do
|
860
|
+
# find(:first, options)
|
861
|
+
# end
|
862
|
+
# else
|
863
|
+
# find(:first, options.merge(finder_options))
|
864
|
+
# end
|
865
|
+
# end
|
866
|
+
self.class_eval <<-EOC, __FILE__, __LINE__
|
867
|
+
def self.#{method_id}(*args)
|
868
|
+
options = args.extract_options!
|
869
|
+
attributes = construct_attributes_from_arguments(
|
870
|
+
[:#{attribute_names.join(',:')}],
|
871
|
+
args
|
872
|
+
)
|
873
|
+
finder_options = { :conditions => attributes }
|
874
|
+
validate_find_options(options)
|
875
|
+
set_readonly_option!(options)
|
876
|
+
|
877
|
+
#{'result = ' if bang}if options[:conditions]
|
878
|
+
with_scope(:find => finder_options) do
|
879
|
+
find(:#{finder}, options)
|
880
|
+
end
|
881
|
+
else
|
882
|
+
find(:#{finder}, options.merge(finder_options))
|
883
|
+
end
|
884
|
+
#{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
|
885
|
+
end
|
886
|
+
EOC
|
887
|
+
send(method_id, *arguments)
|
888
|
+
elsif match.instantiator?
|
889
|
+
instantiator = match.instantiator
|
890
|
+
# def self.find_or_create_by_user_id(*args)
|
891
|
+
# guard_protected_attributes = false
|
892
|
+
#
|
893
|
+
# if args[0].is_a?(Hash)
|
894
|
+
# guard_protected_attributes = true
|
895
|
+
# attributes = args[0].with_indifferent_access
|
896
|
+
# find_attributes = attributes.slice(*[:user_id])
|
897
|
+
# else
|
898
|
+
# find_attributes = attributes = construct_attributes_from_arguments([:user_id], args)
|
899
|
+
# end
|
900
|
+
#
|
901
|
+
# options = { :conditions => find_attributes }
|
902
|
+
# set_readonly_option!(options)
|
903
|
+
#
|
904
|
+
# record = find(:first, options)
|
905
|
+
#
|
906
|
+
# if record.nil?
|
907
|
+
# record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
|
908
|
+
# yield(record) if block_given?
|
909
|
+
# record.save
|
910
|
+
# record
|
911
|
+
# else
|
912
|
+
# record
|
913
|
+
# end
|
914
|
+
# end
|
915
|
+
self.class_eval <<-EOC, __FILE__, __LINE__
|
916
|
+
def self.#{method_id}(*args)
|
917
|
+
guard_protected_attributes = false
|
918
|
+
|
919
|
+
if args[0].is_a?(Hash)
|
920
|
+
guard_protected_attributes = true
|
921
|
+
attributes = args[0].with_indifferent_access
|
922
|
+
find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
|
923
|
+
else
|
924
|
+
find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
|
925
|
+
end
|
926
|
+
|
927
|
+
options = { :conditions => find_attributes }
|
928
|
+
set_readonly_option!(options)
|
929
|
+
|
930
|
+
record = find(:first, options)
|
931
|
+
|
932
|
+
if record.nil?
|
933
|
+
record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
|
934
|
+
#{'yield(record) if block_given?'}
|
935
|
+
#{'record.save' if instantiator == :create}
|
936
|
+
record
|
937
|
+
else
|
938
|
+
record
|
939
|
+
end
|
940
|
+
end
|
941
|
+
EOC
|
942
|
+
send(method_id, *arguments, &block)
|
943
|
+
end
|
944
|
+
elsif match = ActiveRecord::DynamicScopeMatch.match(method_id)
|
945
|
+
attribute_names = match.attribute_names
|
946
|
+
super unless all_attributes_exists?(attribute_names)
|
947
|
+
if match.scope?
|
948
|
+
self.class_eval <<-EOC, __FILE__, __LINE__
|
949
|
+
def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
|
950
|
+
options = args.extract_options! # options = args.extract_options!
|
951
|
+
attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments(
|
952
|
+
[:#{attribute_names.join(',:')}], args # [:user_name, :password], args
|
953
|
+
) # )
|
954
|
+
#
|
955
|
+
scoped(:conditions => attributes) # scoped(:conditions => attributes)
|
956
|
+
end # end
|
957
|
+
EOC
|
958
|
+
send(method_id, *arguments)
|
959
|
+
end
|
960
|
+
else
|
961
|
+
super
|
962
|
+
end
|
963
|
+
end
|
964
|
+
|
965
|
+
def construct_attributes_from_arguments(attribute_names, arguments)
|
966
|
+
attributes = {}
|
967
|
+
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
|
968
|
+
attributes
|
969
|
+
end
|
970
|
+
|
971
|
+
# Similar in purpose to +expand_hash_conditions_for_aggregates+.
|
972
|
+
def expand_attribute_names_for_aggregates(attribute_names)
|
973
|
+
expanded_attribute_names = []
|
974
|
+
attribute_names.each do |attribute_name|
|
975
|
+
unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
|
976
|
+
aggregate_mapping(aggregation).each do |field_attr, aggregate_attr|
|
977
|
+
expanded_attribute_names << field_attr
|
978
|
+
end
|
979
|
+
else
|
980
|
+
expanded_attribute_names << attribute_name
|
981
|
+
end
|
982
|
+
end
|
983
|
+
expanded_attribute_names
|
984
|
+
end
|
985
|
+
|
986
|
+
def all_attributes_exists?(attribute_names)
|
987
|
+
attribute_names = expand_attribute_names_for_aggregates(attribute_names)
|
988
|
+
attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) }
|
989
|
+
end
|
990
|
+
|
991
|
+
# Nest the type name in the same module as this class.
|
992
|
+
# Bar is "MyApp::Business::Bar" relative to MyApp::Business::Foo
|
993
|
+
def type_name_with_module(type_name)
|
994
|
+
if store_full_sti_class
|
995
|
+
type_name
|
996
|
+
else
|
997
|
+
(/^::/ =~ type_name) ? type_name : "#{parent.name}::#{type_name}"
|
998
|
+
end
|
999
|
+
end
|
1000
|
+
|
1001
|
+
# Test whether the given method and optional key are scoped.
|
1002
|
+
def scoped?(method, key = nil) #:nodoc:
|
1003
|
+
if current_scoped_methods && (scope = current_scoped_methods[method])
|
1004
|
+
!key || !scope[key].nil?
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
# Retrieve the scope for the given method and optional key.
|
1009
|
+
def scope(method, key = nil) #:nodoc:
|
1010
|
+
if current_scoped_methods && (scope = current_scoped_methods[method])
|
1011
|
+
key ? scope[key] : scope
|
1012
|
+
end
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
def scoped_methods #:nodoc:
|
1016
|
+
Thread.current[:"#{self}_scoped_methods"] ||= default_scoping.dup
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
def current_scoped_methods #:nodoc:
|
1020
|
+
scoped_methods.last
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
# Returns the class type of the record using the current module as a prefix. So descendants of
|
1024
|
+
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
|
1025
|
+
def compute_type(type_name)
|
1026
|
+
modularized_name = type_name_with_module(type_name)
|
1027
|
+
silence_warnings do
|
1028
|
+
begin
|
1029
|
+
class_eval(modularized_name, __FILE__, __LINE__)
|
1030
|
+
rescue NameError
|
1031
|
+
class_eval(type_name, __FILE__, __LINE__)
|
1032
|
+
end
|
1033
|
+
end
|
1034
|
+
end
|
1035
|
+
|
1036
|
+
# Returns the class descending directly from ActiveGroonga::Base or an
|
1037
|
+
# abstract class, if any, in the inheritance hierarchy.
|
1038
|
+
def class_of_active_groonga_descendant(klass)
|
1039
|
+
if klass.superclass == Base || klass.superclass.abstract_class?
|
1040
|
+
klass
|
1041
|
+
elsif klass.superclass.nil?
|
1042
|
+
raise ActiveGroongaError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
|
1043
|
+
else
|
1044
|
+
class_of_active_record_descendant(klass.superclass)
|
1045
|
+
end
|
1046
|
+
end
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
def initialize(attributes=nil)
|
1050
|
+
@id = nil
|
1051
|
+
@score = nil
|
1052
|
+
@attributes = attributes_from_column_definition
|
1053
|
+
@attributes_cache = {}
|
1054
|
+
@new_record = true
|
1055
|
+
ensure_proper_type
|
1056
|
+
self.attributes = attributes unless attributes.nil?
|
1057
|
+
self.class.send(:scope, :create).each { |att,value| self.send("#{att}=", value) } if self.class.send(:scoped?, :create)
|
1058
|
+
result = yield self if block_given?
|
1059
|
+
callback(:after_initialize) if respond_to_without_attributes?(:after_initialize)
|
1060
|
+
result
|
1061
|
+
end
|
1062
|
+
|
1063
|
+
# A model instance's primary key is always available as model.id
|
1064
|
+
# whether you name it the default 'id' or set it to something else.
|
1065
|
+
def id
|
1066
|
+
@id
|
1067
|
+
end
|
1068
|
+
|
1069
|
+
def score
|
1070
|
+
@score
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
# Returns a String, which Action Pack uses for constructing an URL to this
|
1074
|
+
# object. The default implementation returns this record's id as a String,
|
1075
|
+
# or nil if this record's unsaved.
|
1076
|
+
#
|
1077
|
+
# For example, suppose that you have a User model, and that you have a
|
1078
|
+
# <tt>map.resources :users</tt> route. Normally, +user_path+ will
|
1079
|
+
# construct a path with the user object's 'id' in it:
|
1080
|
+
#
|
1081
|
+
# user = User.find_by_name('Phusion')
|
1082
|
+
# user_path(user) # => "/users/1"
|
1083
|
+
#
|
1084
|
+
# You can override +to_param+ in your model to make +user_path+ construct
|
1085
|
+
# a path using the user's name instead of the user's id:
|
1086
|
+
#
|
1087
|
+
# class User < ActiveRecord::Base
|
1088
|
+
# def to_param # overridden
|
1089
|
+
# name
|
1090
|
+
# end
|
1091
|
+
# end
|
1092
|
+
#
|
1093
|
+
# user = User.find_by_name('Phusion')
|
1094
|
+
# user_path(user) # => "/users/Phusion"
|
1095
|
+
def to_param
|
1096
|
+
# We can't use alias_method here, because method 'id' optimizes itself on the fly.
|
1097
|
+
(id = self.id) ? id.to_s : nil # Be sure to stringify the id for routes
|
1098
|
+
end
|
1099
|
+
|
1100
|
+
# Sets the primary ID.
|
1101
|
+
def id=(value)
|
1102
|
+
@id = value
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
# Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet; otherwise, returns false.
|
1106
|
+
def new_record?
|
1107
|
+
@new_record || false
|
1108
|
+
end
|
1109
|
+
|
1110
|
+
# :call-seq:
|
1111
|
+
# save(perform_validation = true)
|
1112
|
+
#
|
1113
|
+
# Saves the model.
|
1114
|
+
#
|
1115
|
+
# If the model is new a record gets created in the database, otherwise
|
1116
|
+
# the existing record gets updated.
|
1117
|
+
#
|
1118
|
+
# If +perform_validation+ is true validations run. If any of them fail
|
1119
|
+
# the action is cancelled and +save+ returns +false+. If the flag is
|
1120
|
+
# false validations are bypassed altogether. See
|
1121
|
+
# ActiveRecord::Validations for more information.
|
1122
|
+
#
|
1123
|
+
# There's a series of callbacks associated with +save+. If any of the
|
1124
|
+
# <tt>before_*</tt> callbacks return +false+ the action is cancelled and
|
1125
|
+
# +save+ returns +false+. See ActiveRecord::Callbacks for further
|
1126
|
+
# details.
|
1127
|
+
def save
|
1128
|
+
create_or_update
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
# Saves the model.
|
1132
|
+
#
|
1133
|
+
# If the model is new a record gets created in the database, otherwise
|
1134
|
+
# the existing record gets updated.
|
1135
|
+
#
|
1136
|
+
# With <tt>save!</tt> validations always run. If any of them fail
|
1137
|
+
# ActiveGroonga::RecordInvalid gets raised. See ActiveRecord::Validations
|
1138
|
+
# for more information.
|
1139
|
+
#
|
1140
|
+
# There's a series of callbacks associated with <tt>save!</tt>. If any of
|
1141
|
+
# the <tt>before_*</tt> callbacks return +false+ the action is cancelled
|
1142
|
+
# and <tt>save!</tt> raises ActiveGroonga::RecordNotSaved. See
|
1143
|
+
# ActiveRecord::Callbacks for further details.
|
1144
|
+
def save!
|
1145
|
+
create_or_update || raise(RecordNotSaved)
|
1146
|
+
end
|
1147
|
+
|
1148
|
+
# Deletes the record in the database and freezes this instance to
|
1149
|
+
# reflect that no changes should be made (since they can't be
|
1150
|
+
# persisted). Returns the frozen instance.
|
1151
|
+
#
|
1152
|
+
# The row is simply removed with a SQL +DELETE+ statement on the
|
1153
|
+
# record's primary key, and no callbacks are executed.
|
1154
|
+
#
|
1155
|
+
# To enforce the object's +before_destroy+ and +after_destroy+
|
1156
|
+
# callbacks, Observer methods, or any <tt>:dependent</tt> association
|
1157
|
+
# options, use <tt>#destroy</tt>.
|
1158
|
+
def delete
|
1159
|
+
self.class.delete(id) unless new_record?
|
1160
|
+
freeze
|
1161
|
+
end
|
1162
|
+
|
1163
|
+
# Deletes the record in the database and freezes this instance to reflect that no changes should
|
1164
|
+
# be made (since they can't be persisted).
|
1165
|
+
def destroy
|
1166
|
+
self.class.table.delete(id) unless new_record?
|
1167
|
+
freeze
|
1168
|
+
end
|
1169
|
+
|
1170
|
+
# Updates a single attribute and saves the record without going through the normal validation procedure.
|
1171
|
+
# This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
|
1172
|
+
# in Base is replaced with this when the validations module is mixed in, which it is by default.
|
1173
|
+
def update_attribute(name, value)
|
1174
|
+
send(name.to_s + '=', value)
|
1175
|
+
save(false)
|
1176
|
+
end
|
1177
|
+
|
1178
|
+
# Updates all the attributes from the passed-in Hash and saves the record. If the object is invalid, the saving will
|
1179
|
+
# fail and false will be returned.
|
1180
|
+
def update_attributes(attributes)
|
1181
|
+
self.attributes = attributes
|
1182
|
+
save
|
1183
|
+
end
|
1184
|
+
|
1185
|
+
# Updates an object just like Base.update_attributes but calls save! instead of save so an exception is raised if the record is invalid.
|
1186
|
+
def update_attributes!(attributes)
|
1187
|
+
self.attributes = attributes
|
1188
|
+
save!
|
1189
|
+
end
|
1190
|
+
|
1191
|
+
# Reloads the attributes of this object from the database.
|
1192
|
+
# The optional options argument is passed to find when reloading so you
|
1193
|
+
# may do e.g. record.reload(:lock => true) to reload the same record with
|
1194
|
+
# an exclusive row lock.
|
1195
|
+
def reload(options = nil)
|
1196
|
+
clear_aggregation_cache
|
1197
|
+
clear_association_cache
|
1198
|
+
@attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes'))
|
1199
|
+
@attributes_cache = {}
|
1200
|
+
self
|
1201
|
+
end
|
1202
|
+
|
1203
|
+
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
|
1204
|
+
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
|
1205
|
+
# (Alias for the protected read_attribute method).
|
1206
|
+
def [](attr_name)
|
1207
|
+
read_attribute(attr_name)
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
|
1211
|
+
# (Alias for the protected write_attribute method).
|
1212
|
+
def []=(attr_name, value)
|
1213
|
+
write_attribute(attr_name, value)
|
1214
|
+
end
|
1215
|
+
|
1216
|
+
# Allows you to set all the attributes at once by passing in a hash with keys
|
1217
|
+
# matching the attribute names (which again matches the column names).
|
1218
|
+
#
|
1219
|
+
# If +guard_protected_attributes+ is true (the default), then sensitive
|
1220
|
+
# attributes can be protected from this form of mass-assignment by using
|
1221
|
+
# the +attr_protected+ macro. Or you can alternatively specify which
|
1222
|
+
# attributes *can* be accessed with the +attr_accessible+ macro. Then all the
|
1223
|
+
# attributes not included in that won't be allowed to be mass-assigned.
|
1224
|
+
#
|
1225
|
+
# class User < ActiveGroonga::Base
|
1226
|
+
# attr_protected :is_admin
|
1227
|
+
# end
|
1228
|
+
#
|
1229
|
+
# user = User.new
|
1230
|
+
# user.attributes = { :username => 'Phusion', :is_admin => true }
|
1231
|
+
# user.username # => "Phusion"
|
1232
|
+
# user.is_admin? # => false
|
1233
|
+
#
|
1234
|
+
# user.send(:attributes=, { :username => 'Phusion', :is_admin => true }, false)
|
1235
|
+
# user.is_admin? # => true
|
1236
|
+
def attributes=(new_attributes, guard_protected_attributes = true)
|
1237
|
+
return if new_attributes.nil?
|
1238
|
+
attributes = new_attributes.dup
|
1239
|
+
attributes.stringify_keys!
|
1240
|
+
|
1241
|
+
multi_parameter_attributes = []
|
1242
|
+
attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes
|
1243
|
+
|
1244
|
+
attributes.each do |k, v|
|
1245
|
+
if k.include?("(")
|
1246
|
+
multi_parameter_attributes << [ k, v ]
|
1247
|
+
else
|
1248
|
+
respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
|
1249
|
+
end
|
1250
|
+
end
|
1251
|
+
|
1252
|
+
assign_multiparameter_attributes(multi_parameter_attributes)
|
1253
|
+
end
|
1254
|
+
|
1255
|
+
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
|
1256
|
+
def attributes
|
1257
|
+
self.attribute_names.inject({}) do |attrs, name|
|
1258
|
+
attrs[name] = read_attribute(name)
|
1259
|
+
attrs
|
1260
|
+
end
|
1261
|
+
end
|
1262
|
+
|
1263
|
+
# Returns a hash of attributes before typecasting and deserialization.
|
1264
|
+
def attributes_before_type_cast
|
1265
|
+
self.attribute_names.inject({}) do |attrs, name|
|
1266
|
+
attrs[name] = read_attribute_before_type_cast(name)
|
1267
|
+
attrs
|
1268
|
+
end
|
1269
|
+
end
|
1270
|
+
|
1271
|
+
# Returns an <tt>#inspect</tt>-like string for the value of the
|
1272
|
+
# attribute +attr_name+. String attributes are elided after 50
|
1273
|
+
# characters, and Date and Time attributes are returned in the
|
1274
|
+
# <tt>:db</tt> format. Other attributes return the value of
|
1275
|
+
# <tt>#inspect</tt> without modification.
|
1276
|
+
#
|
1277
|
+
# person = Person.create!(:name => "David Heinemeier Hansson " * 3)
|
1278
|
+
#
|
1279
|
+
# person.attribute_for_inspect(:name)
|
1280
|
+
# # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
|
1281
|
+
#
|
1282
|
+
# person.attribute_for_inspect(:created_at)
|
1283
|
+
# # => '"2009-01-12 04:48:57"'
|
1284
|
+
def attribute_for_inspect(attr_name)
|
1285
|
+
value = read_attribute(attr_name)
|
1286
|
+
|
1287
|
+
if value.is_a?(String) && value.length > 50
|
1288
|
+
"#{value[0..50]}...".inspect
|
1289
|
+
elsif value.is_a?(Date) || value.is_a?(Time)
|
1290
|
+
%("#{value.to_s(:db)}")
|
1291
|
+
else
|
1292
|
+
value.inspect
|
1293
|
+
end
|
1294
|
+
end
|
1295
|
+
|
1296
|
+
# Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
|
1297
|
+
# nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
|
1298
|
+
def attribute_present?(attribute)
|
1299
|
+
value = read_attribute(attribute)
|
1300
|
+
!value.blank?
|
1301
|
+
end
|
1302
|
+
|
1303
|
+
# Returns true if the given attribute is in the attributes hash
|
1304
|
+
def has_attribute?(attr_name)
|
1305
|
+
@attributes.has_key?(attr_name.to_s)
|
1306
|
+
end
|
1307
|
+
|
1308
|
+
# Returns an array of names for the attributes available on this object sorted alphabetically.
|
1309
|
+
def attribute_names
|
1310
|
+
@attributes.keys.sort
|
1311
|
+
end
|
1312
|
+
|
1313
|
+
# Returns the column object for the named attribute.
|
1314
|
+
def column_for_attribute(name)
|
1315
|
+
self.class.columns_hash[name.to_s]
|
1316
|
+
end
|
1317
|
+
|
1318
|
+
# Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id.
|
1319
|
+
def ==(comparison_object)
|
1320
|
+
comparison_object.equal?(self) ||
|
1321
|
+
(comparison_object.instance_of?(self.class) &&
|
1322
|
+
comparison_object.id == id &&
|
1323
|
+
!comparison_object.new_record?)
|
1324
|
+
end
|
1325
|
+
|
1326
|
+
# Delegates to ==
|
1327
|
+
def eql?(comparison_object)
|
1328
|
+
self == (comparison_object)
|
1329
|
+
end
|
1330
|
+
|
1331
|
+
# Delegates to id in order to allow two records of the same type and id to work with something like:
|
1332
|
+
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
|
1333
|
+
def hash
|
1334
|
+
id.hash
|
1335
|
+
end
|
1336
|
+
|
1337
|
+
# Freeze the attributes hash such that associations are still accessible, even on destroyed records.
|
1338
|
+
def freeze
|
1339
|
+
@attributes.freeze; self
|
1340
|
+
end
|
1341
|
+
|
1342
|
+
# Returns +true+ if the attributes hash has been frozen.
|
1343
|
+
def frozen?
|
1344
|
+
@attributes.frozen?
|
1345
|
+
end
|
1346
|
+
|
1347
|
+
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
|
1348
|
+
# attributes will be marked as read only since they cannot be saved.
|
1349
|
+
def readonly?
|
1350
|
+
defined?(@readonly) && @readonly == true
|
1351
|
+
end
|
1352
|
+
|
1353
|
+
# Marks this record as read only.
|
1354
|
+
def readonly!
|
1355
|
+
@readonly = true
|
1356
|
+
end
|
1357
|
+
|
1358
|
+
# Returns the contents of the record as a nicely formatted string.
|
1359
|
+
def inspect
|
1360
|
+
attributes_as_nice_string = self.class.column_names.collect { |name|
|
1361
|
+
if has_attribute?(name) || new_record?
|
1362
|
+
"#{name}: #{attribute_for_inspect(name)}"
|
1363
|
+
end
|
1364
|
+
}.compact.join(", ")
|
1365
|
+
"#<#{self.class} #{attributes_as_nice_string}>"
|
1366
|
+
end
|
1367
|
+
|
1368
|
+
private
|
1369
|
+
def create_or_update
|
1370
|
+
raise ReadOnlyRecord if readonly?
|
1371
|
+
result = new_record? ? create : update
|
1372
|
+
result != false
|
1373
|
+
end
|
1374
|
+
|
1375
|
+
# Updates the associated record with values matching those of the instance attributes.
|
1376
|
+
# Returns the number of affected rows.
|
1377
|
+
def update(attribute_names=@attributes.keys)
|
1378
|
+
attribute_names = remove_readonly_attributes(attribute_names)
|
1379
|
+
table = self.class.table
|
1380
|
+
indexes = Schema.indexes(table)
|
1381
|
+
quoted_attributes = attributes_with_quotes(false, attribute_names)
|
1382
|
+
quoted_attributes.each do |name, value|
|
1383
|
+
column = table.column(name)
|
1384
|
+
next if column.nil?
|
1385
|
+
column[id] = value
|
1386
|
+
end
|
1387
|
+
end
|
1388
|
+
|
1389
|
+
# Creates a record with values matching those of the instance attributes
|
1390
|
+
# and returns its id.
|
1391
|
+
def create
|
1392
|
+
table = self.class.table
|
1393
|
+
record = table.add
|
1394
|
+
indexes = Schema.indexes(table)
|
1395
|
+
quoted_attributes = attributes_with_quotes
|
1396
|
+
quoted_attributes.each do |name, value|
|
1397
|
+
record[name] = value
|
1398
|
+
end
|
1399
|
+
self.id = record.id
|
1400
|
+
@new_record = false
|
1401
|
+
id
|
1402
|
+
end
|
1403
|
+
|
1404
|
+
# Sets the attribute used for single table inheritance to this class name if this is not the ActiveRecord::Base descendant.
|
1405
|
+
# Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to do Reply.new without having to
|
1406
|
+
# set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. No such attribute would be set for objects of the
|
1407
|
+
# Message class in that example.
|
1408
|
+
def ensure_proper_type
|
1409
|
+
unless self.class.descends_from_active_groonga?
|
1410
|
+
write_attribute(self.class.inheritance_column, self.class.sti_name)
|
1411
|
+
end
|
1412
|
+
end
|
1413
|
+
|
1414
|
+
def convert_number_column_value(value)
|
1415
|
+
if value == false
|
1416
|
+
0
|
1417
|
+
elsif value == true
|
1418
|
+
1
|
1419
|
+
elsif value.is_a?(String) && value.blank?
|
1420
|
+
nil
|
1421
|
+
else
|
1422
|
+
value
|
1423
|
+
end
|
1424
|
+
end
|
1425
|
+
|
1426
|
+
def remove_attributes_protected_from_mass_assignment(attributes)
|
1427
|
+
safe_attributes =
|
1428
|
+
if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil?
|
1429
|
+
attributes.reject { |key, value| attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
|
1430
|
+
elsif self.class.protected_attributes.nil?
|
1431
|
+
attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.gsub(/\(.+/, "")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
|
1432
|
+
elsif self.class.accessible_attributes.nil?
|
1433
|
+
attributes.reject { |key, value| self.class.protected_attributes.include?(key.gsub(/\(.+/,"")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
|
1434
|
+
else
|
1435
|
+
raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both."
|
1436
|
+
end
|
1437
|
+
|
1438
|
+
removed_attributes = attributes.keys - safe_attributes.keys
|
1439
|
+
|
1440
|
+
if removed_attributes.any?
|
1441
|
+
log_protected_attribute_removal(removed_attributes)
|
1442
|
+
end
|
1443
|
+
|
1444
|
+
safe_attributes
|
1445
|
+
end
|
1446
|
+
|
1447
|
+
# Removes attributes which have been marked as readonly.
|
1448
|
+
def remove_readonly_attributes(attributes)
|
1449
|
+
unless self.class.readonly_attributes.nil?
|
1450
|
+
attributes.delete_if { |key, value| self.class.readonly_attributes.include?(key.gsub(/\(.+/,"")) }
|
1451
|
+
else
|
1452
|
+
attributes
|
1453
|
+
end
|
1454
|
+
end
|
1455
|
+
|
1456
|
+
def log_protected_attribute_removal(*attributes)
|
1457
|
+
logger.debug "WARNING: Can't mass-assign these protected attributes: #{attributes.join(', ')}"
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
# The primary key and inheritance column can never be set by mass-assignment for security reasons.
|
1461
|
+
def attributes_protected_by_default
|
1462
|
+
default = [ self.class.primary_key, self.class.inheritance_column ]
|
1463
|
+
default << 'id' unless self.class.primary_key.eql? 'id'
|
1464
|
+
default
|
1465
|
+
end
|
1466
|
+
|
1467
|
+
# Initializes the attributes array with keys matching the columns from the linked table and
|
1468
|
+
# the values matching the corresponding default value of that column, so
|
1469
|
+
# that a new instance, or one populated from a passed-in Hash, still has all the attributes
|
1470
|
+
# that instances loaded from the database would.
|
1471
|
+
def attributes_from_column_definition
|
1472
|
+
self.class.columns.inject({}) do |attributes, column|
|
1473
|
+
attributes[column.name] = column.default
|
1474
|
+
attributes
|
1475
|
+
end
|
1476
|
+
end
|
1477
|
+
|
1478
|
+
# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
|
1479
|
+
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
|
1480
|
+
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
|
1481
|
+
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
|
1482
|
+
# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float,
|
1483
|
+
# s for String, and a for Array. If all the values for a given attribute are empty, the attribute will be set to nil.
|
1484
|
+
def assign_multiparameter_attributes(pairs)
|
1485
|
+
execute_callstack_for_multiparameter_attributes(
|
1486
|
+
extract_callstack_for_multiparameter_attributes(pairs)
|
1487
|
+
)
|
1488
|
+
end
|
1489
|
+
|
1490
|
+
def execute_callstack_for_multiparameter_attributes(callstack)
|
1491
|
+
errors = []
|
1492
|
+
callstack.each do |name, values|
|
1493
|
+
klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
|
1494
|
+
if values.empty?
|
1495
|
+
send(name + "=", nil)
|
1496
|
+
else
|
1497
|
+
begin
|
1498
|
+
value = if Time == klass
|
1499
|
+
instantiate_time_object(name, values)
|
1500
|
+
elsif Date == klass
|
1501
|
+
begin
|
1502
|
+
Date.new(*values)
|
1503
|
+
rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
|
1504
|
+
instantiate_time_object(name, values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
|
1505
|
+
end
|
1506
|
+
else
|
1507
|
+
klass.new(*values)
|
1508
|
+
end
|
1509
|
+
|
1510
|
+
send(name + "=", value)
|
1511
|
+
rescue => ex
|
1512
|
+
errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
|
1513
|
+
end
|
1514
|
+
end
|
1515
|
+
end
|
1516
|
+
unless errors.empty?
|
1517
|
+
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
|
1518
|
+
end
|
1519
|
+
end
|
1520
|
+
|
1521
|
+
def extract_callstack_for_multiparameter_attributes(pairs)
|
1522
|
+
attributes = { }
|
1523
|
+
|
1524
|
+
for pair in pairs
|
1525
|
+
multiparameter_name, value = pair
|
1526
|
+
attribute_name = multiparameter_name.split("(").first
|
1527
|
+
attributes[attribute_name] = [] unless attributes.include?(attribute_name)
|
1528
|
+
|
1529
|
+
unless value.empty?
|
1530
|
+
attributes[attribute_name] <<
|
1531
|
+
[ find_parameter_position(multiparameter_name), type_cast_attribute_value(multiparameter_name, value) ]
|
1532
|
+
end
|
1533
|
+
end
|
1534
|
+
|
1535
|
+
attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }
|
1536
|
+
end
|
1537
|
+
|
1538
|
+
# Returns a copy of the attributes hash where all the values have been safely quoted for use in
|
1539
|
+
# an SQL statement.
|
1540
|
+
def attributes_with_quotes(include_readonly_attributes=true, attribute_names=@attributes.keys)
|
1541
|
+
quoted = {}
|
1542
|
+
attribute_names.each do |name|
|
1543
|
+
column = column_for_attribute(name)
|
1544
|
+
next if column.nil?
|
1545
|
+
|
1546
|
+
value = read_attribute(name)
|
1547
|
+
# We need explicit to_yaml because quote() does not properly convert Time/Date fields to YAML.
|
1548
|
+
if value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))
|
1549
|
+
value = value.to_yaml
|
1550
|
+
end
|
1551
|
+
quoted[name] = column.quote(value)
|
1552
|
+
end
|
1553
|
+
include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)
|
1554
|
+
end
|
1555
|
+
|
1556
|
+
# Quote strings appropriately for SQL statements.
|
1557
|
+
def quote_value(value, column=nil)
|
1558
|
+
if column
|
1559
|
+
column.quote(value)
|
1560
|
+
else
|
1561
|
+
value
|
1562
|
+
end
|
1563
|
+
end
|
1564
|
+
|
1565
|
+
def clone_attribute_value(reader_method, attribute_name)
|
1566
|
+
value = send(reader_method, attribute_name)
|
1567
|
+
value.duplicable? ? value.clone : value
|
1568
|
+
rescue TypeError, NoMethodError
|
1569
|
+
value
|
1570
|
+
end
|
1571
|
+
|
1572
|
+
include Validations
|
1573
|
+
include AttributeMethods
|
1574
|
+
include Dirty
|
1575
|
+
include Timestamp
|
1576
|
+
include Associations
|
1577
|
+
include Aggregations, Reflection
|
1578
|
+
end
|
1579
|
+
end
|