sequel 3.35.0 → 3.36.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +78 -0
- data/Rakefile +3 -3
- data/bin/sequel +3 -1
- data/doc/advanced_associations.rdoc +154 -11
- data/doc/migration.rdoc +18 -0
- data/doc/object_model.rdoc +541 -0
- data/doc/opening_databases.rdoc +4 -1
- data/doc/release_notes/3.36.0.txt +245 -0
- data/doc/schema_modification.rdoc +0 -6
- data/lib/sequel/adapters/do/mysql.rb +7 -0
- data/lib/sequel/adapters/jdbc.rb +11 -3
- data/lib/sequel/adapters/jdbc/mysql.rb +3 -5
- data/lib/sequel/adapters/jdbc/postgresql.rb +10 -8
- data/lib/sequel/adapters/jdbc/progress.rb +21 -0
- data/lib/sequel/adapters/mock.rb +2 -6
- data/lib/sequel/adapters/mysql.rb +3 -9
- data/lib/sequel/adapters/mysql2.rb +12 -11
- data/lib/sequel/adapters/postgres.rb +32 -40
- data/lib/sequel/adapters/shared/mssql.rb +15 -11
- data/lib/sequel/adapters/shared/mysql.rb +28 -3
- data/lib/sequel/adapters/shared/oracle.rb +5 -0
- data/lib/sequel/adapters/shared/postgres.rb +59 -5
- data/lib/sequel/adapters/shared/sqlite.rb +3 -13
- data/lib/sequel/adapters/sqlite.rb +0 -7
- data/lib/sequel/adapters/swift/mysql.rb +2 -5
- data/lib/sequel/adapters/tinytds.rb +1 -2
- data/lib/sequel/connection_pool/sharded_threaded.rb +5 -1
- data/lib/sequel/connection_pool/threaded.rb +9 -1
- data/lib/sequel/database/dataset_defaults.rb +3 -1
- data/lib/sequel/database/misc.rb +7 -1
- data/lib/sequel/database/query.rb +11 -3
- data/lib/sequel/database/schema_generator.rb +40 -9
- data/lib/sequel/database/schema_methods.rb +6 -1
- data/lib/sequel/dataset/actions.rb +5 -5
- data/lib/sequel/dataset/prepared_statements.rb +3 -1
- data/lib/sequel/dataset/query.rb +1 -1
- data/lib/sequel/extensions/migration.rb +28 -0
- data/lib/sequel/extensions/pg_auto_parameterize.rb +0 -9
- data/lib/sequel/extensions/pg_inet.rb +89 -0
- data/lib/sequel/extensions/pg_json.rb +178 -0
- data/lib/sequel/extensions/schema_dumper.rb +24 -6
- data/lib/sequel/model/associations.rb +19 -15
- data/lib/sequel/model/base.rb +11 -12
- data/lib/sequel/plugins/composition.rb +1 -2
- data/lib/sequel/plugins/eager_each.rb +59 -0
- data/lib/sequel/plugins/json_serializer.rb +41 -4
- data/lib/sequel/plugins/nested_attributes.rb +72 -52
- data/lib/sequel/plugins/optimistic_locking.rb +8 -0
- data/lib/sequel/plugins/tactical_eager_loading.rb +7 -7
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/postgres_spec.rb +271 -1
- data/spec/adapters/sqlite_spec.rb +11 -0
- data/spec/core/connection_pool_spec.rb +26 -1
- data/spec/core/database_spec.rb +19 -0
- data/spec/core/dataset_spec.rb +45 -5
- data/spec/core/expression_filters_spec.rb +31 -67
- data/spec/core/mock_adapter_spec.rb +4 -0
- data/spec/extensions/core_extensions_spec.rb +83 -0
- data/spec/extensions/eager_each_spec.rb +34 -0
- data/spec/extensions/inflector_spec.rb +0 -4
- data/spec/extensions/json_serializer_spec.rb +32 -1
- data/spec/extensions/migration_spec.rb +28 -0
- data/spec/extensions/nested_attributes_spec.rb +134 -1
- data/spec/extensions/optimistic_locking_spec.rb +15 -1
- data/spec/extensions/pg_hstore_spec.rb +1 -1
- data/spec/extensions/pg_inet_spec.rb +44 -0
- data/spec/extensions/pg_json_spec.rb +101 -0
- data/spec/extensions/prepared_statements_spec.rb +30 -0
- data/spec/extensions/rcte_tree_spec.rb +9 -0
- data/spec/extensions/schema_dumper_spec.rb +195 -7
- data/spec/extensions/serialization_spec.rb +4 -0
- data/spec/extensions/spec_helper.rb +9 -1
- data/spec/extensions/tactical_eager_loading_spec.rb +8 -0
- data/spec/integration/database_test.rb +5 -1
- data/spec/integration/prepared_statement_test.rb +20 -2
- data/spec/model/associations_spec.rb +27 -0
- data/spec/model/base_spec.rb +54 -0
- data/spec/model/model_spec.rb +6 -0
- data/spec/model/record_spec.rb +18 -0
- data/spec/rcov.opts +2 -0
- metadata +14 -3
@@ -0,0 +1,178 @@
|
|
1
|
+
# The pg_json extension adds support for Sequel to handle
|
2
|
+
# PostgreSQL's json type. It is slightly more strict than the
|
3
|
+
# PostgreSQL json type in that the object returned must be an
|
4
|
+
# array or object (PostgreSQL's json type considers plain numbers
|
5
|
+
# and strings as valid). This is because Sequel relies completely
|
6
|
+
# on the ruby JSON library for parsing, and ruby's JSON library
|
7
|
+
# does not accept the values.
|
8
|
+
#
|
9
|
+
# This extension integrates with Sequel's native postgres adapter, so
|
10
|
+
# that when json fields are retrieved, they are parsed and returned
|
11
|
+
# as instances of Sequel::Postgres::JSONArray or
|
12
|
+
# Sequel::Postgres::JSONHash. JSONArray and JSONHash are
|
13
|
+
# DelegateClasses of Array and Hash, so they mostly act the same, but
|
14
|
+
# not completely (json_array.is_a?(Array) is false). If you want
|
15
|
+
# the actual array for a JSONArray, call JSONArray#to_a. If you want
|
16
|
+
# the actual hash for a JSONHash, call JSONHash#to_hash.
|
17
|
+
# This is done so that Sequel does not treat JSONArray and JSONHash
|
18
|
+
# like Array and Hash by default, which would cause issues.
|
19
|
+
#
|
20
|
+
# To turn an existing Array or Hash into a JSONArray or JSONHash:
|
21
|
+
#
|
22
|
+
# array.pg_json
|
23
|
+
# hash.pg_json
|
24
|
+
#
|
25
|
+
# So if you want to insert an array or hash into an json database column:
|
26
|
+
#
|
27
|
+
# DB[:table].insert(:column=>[1, 2, 3].pg_json)
|
28
|
+
# DB[:table].insert(:column=>{'a'=>1, 'b'=>2}.pg_json)
|
29
|
+
#
|
30
|
+
# If you would like to use PostgreSQL json columns in your model
|
31
|
+
# objects, you probably want to modify the schema parsing/typecasting
|
32
|
+
# so that it recognizes and correctly handles the json type, which
|
33
|
+
# you can do by:
|
34
|
+
#
|
35
|
+
# DB.extend Sequel::Postgres::JSONDatabaseMethods
|
36
|
+
#
|
37
|
+
# If you are not using the native postgres adapter, you probably
|
38
|
+
# also want to use the typecast_on_load plugin in the model, and
|
39
|
+
# set it to typecast the json column(s) on load.
|
40
|
+
#
|
41
|
+
# The extension is designed to be used with the json type natively
|
42
|
+
# supported by PostgreSQL 9.2+. There was also a PostgreSQL extension released
|
43
|
+
# that allowed the json type to be used on PostgreSQL 9.1. To make
|
44
|
+
# this extension support that type in the native adapter, do the
|
45
|
+
# following after loading this extension:
|
46
|
+
#
|
47
|
+
# Sequel::Postgres::PG_NAMED_TYPES = {} unless defined?(Sequel::Postgres::PG_NAMED_TYPES)
|
48
|
+
# Sequel::Postgres::PG_NAMED_TYPES[:json] = Sequel::Postgres::PG_TYPES[114]
|
49
|
+
#
|
50
|
+
# This extension requires both the json and delegate libraries.
|
51
|
+
|
52
|
+
require 'delegate'
|
53
|
+
require 'json'
|
54
|
+
|
55
|
+
module Sequel
|
56
|
+
module Postgres
|
57
|
+
CAST_JSON = '::json'.freeze
|
58
|
+
|
59
|
+
# Class representating PostgreSQL JSON column array values.
|
60
|
+
class JSONArray < DelegateClass(Array)
|
61
|
+
# Convert the array to a string using to_json, append a
|
62
|
+
# literalized version of the string to the sql, and explicitly
|
63
|
+
# cast the string to json.
|
64
|
+
def sql_literal_append(ds, sql)
|
65
|
+
ds.literal_append(sql, to_json)
|
66
|
+
sql << CAST_JSON
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Class representating PostgreSQL JSON column hash/object values.
|
71
|
+
class JSONHash < DelegateClass(Hash)
|
72
|
+
# Convert the array to a string using to_json, append a
|
73
|
+
# literalized version of the string to the sql, and explicitly
|
74
|
+
# cast the string to json.
|
75
|
+
def sql_literal_append(ds, sql)
|
76
|
+
ds.literal_append(sql, to_json)
|
77
|
+
sql << CAST_JSON
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return the object being delegated to.
|
81
|
+
alias to_hash __getobj__
|
82
|
+
end
|
83
|
+
|
84
|
+
# Methods enabling Database object integration with the json type.
|
85
|
+
module JSONDatabaseMethods
|
86
|
+
# Parse the given string as json, returning either a JSONArray
|
87
|
+
# or JSONHash instance, and raising an error if the JSON
|
88
|
+
# parsing does not yield an array or hash.
|
89
|
+
def self.parse_json(s)
|
90
|
+
begin
|
91
|
+
value = JSON.parse(s)
|
92
|
+
rescue JSON::ParserError=>e
|
93
|
+
raise Sequel.convert_exception_class(e, Sequel::InvalidValue)
|
94
|
+
end
|
95
|
+
|
96
|
+
case value
|
97
|
+
when Array
|
98
|
+
JSONArray.new(value)
|
99
|
+
when Hash
|
100
|
+
JSONHash.new(value)
|
101
|
+
else
|
102
|
+
raise Sequel::InvalidValue, "unhandled json value: #{value.inspect} (from #{s.inspect})"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Reset the conversion procs when extending the Database object, so
|
107
|
+
# it will pick up the json convertor. This is only done for the native
|
108
|
+
# postgres adapter.
|
109
|
+
def self.extended(db)
|
110
|
+
db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Handle JSONArray and JSONHash in bound variables
|
114
|
+
def bound_variable_arg(arg, conn)
|
115
|
+
case arg
|
116
|
+
when JSONArray, JSONHash
|
117
|
+
arg.to_json
|
118
|
+
else
|
119
|
+
super
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Make the column type detection recognize the json type.
|
124
|
+
def schema_column_type(db_type)
|
125
|
+
case db_type
|
126
|
+
when 'json'
|
127
|
+
:json
|
128
|
+
else
|
129
|
+
super
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
# Given a value to typecast to the json column
|
136
|
+
# * If given a JSONArray or JSONHash, just return the value
|
137
|
+
# * If given an Array, return a JSONArray
|
138
|
+
# * If given a Hash, return a JSONHash
|
139
|
+
# * If given a String, parse it as would be done during
|
140
|
+
# database retrieval.
|
141
|
+
def typecast_value_json(value)
|
142
|
+
case value
|
143
|
+
when JSONArray, JSONHash
|
144
|
+
value
|
145
|
+
when Array
|
146
|
+
JSONArray.new(value)
|
147
|
+
when Hash
|
148
|
+
JSONHash.new(value)
|
149
|
+
when String
|
150
|
+
JSONDatabaseMethods.parse_json(value)
|
151
|
+
else
|
152
|
+
raise Sequel::InvalidValue, "invalid value for json: #{value.inspect}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
PG_TYPES = {} unless defined?(PG_TYPES)
|
158
|
+
PG_TYPES[114] = JSONDatabaseMethods.method(:parse_json)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class Array
|
163
|
+
# Return a Sequel::Postgres::JSONArray proxy to the receiver.
|
164
|
+
# This is mostly useful as a short cut for creating JSONArray
|
165
|
+
# objects that didn't come from the database.
|
166
|
+
def pg_json
|
167
|
+
Sequel::Postgres::JSONArray.new(self)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
class Hash
|
172
|
+
# Return a Sequel::Postgres::JSONHash proxy to the receiver.
|
173
|
+
# This is mostly useful as a short cut for creating JSONHash
|
174
|
+
# objects that didn't come from the database.
|
175
|
+
def pg_json
|
176
|
+
Sequel::Postgres::JSONHash.new(self)
|
177
|
+
end
|
178
|
+
end
|
@@ -28,6 +28,9 @@ END_MIG
|
|
28
28
|
# the :indexes=>false option to dump_schema_migration. Options:
|
29
29
|
# * :same_db - Create a dump for the same database type, so
|
30
30
|
# don't ignore errors if the index statements fail.
|
31
|
+
# * :index_names - If set to false, don't record names of indexes. If
|
32
|
+
# set to :namespace, prepend the table name to the index name if the
|
33
|
+
# database does not use a global index namespace.
|
31
34
|
def dump_indexes_migration(options={})
|
32
35
|
ts = tables(options)
|
33
36
|
<<END_MIG
|
@@ -53,6 +56,8 @@ END_MIG
|
|
53
56
|
# * :foreign_keys - If set to false, don't dump foreign_keys
|
54
57
|
# * :indexes - If set to false, don't dump indexes (they can be added
|
55
58
|
# later via dump_index_migration).
|
59
|
+
# * :index_names - If set to false, don't record names of indexes. If
|
60
|
+
# set to :namespace, prepend the table name to the index name.
|
56
61
|
def dump_schema_migration(options={})
|
57
62
|
options = options.dup
|
58
63
|
if options[:indexes] == false && !options.has_key?(:foreign_keys)
|
@@ -144,8 +149,14 @@ END_MIG
|
|
144
149
|
# database type is not recognized, return it as a String type.
|
145
150
|
def column_schema_to_ruby_type(schema)
|
146
151
|
case t = schema[:db_type].downcase
|
147
|
-
when /\A(
|
148
|
-
|
152
|
+
when /\A(medium|small)?int(?:eger)?(?:\((\d+)\))?( unsigned)?\z/o
|
153
|
+
if !$1 && $2 && $2.to_i >= 10 && $3
|
154
|
+
# Unsigned integer type with 10 digits can potentially contain values which
|
155
|
+
# don't fit signed integer type, so use bigint type in target database.
|
156
|
+
{:type=>Bignum}
|
157
|
+
else
|
158
|
+
{:type=>Integer}
|
159
|
+
end
|
149
160
|
when /\Atinyint(?:\((\d+)\))?(?: unsigned)?\z/o
|
150
161
|
{:type =>schema[:type] == :boolean ? TrueClass : Integer}
|
151
162
|
when /\Abigint(?:\((?:\d+)\))?(?: unsigned)?\z/o
|
@@ -261,7 +272,7 @@ END_MIG
|
|
261
272
|
Schema::Generator.new(self) do
|
262
273
|
s.each{|name, info| send(*m.call(name, info, options))}
|
263
274
|
primary_key(pks) if !@primary_key && pks.length > 0
|
264
|
-
indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts))} if indexes
|
275
|
+
indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))} if indexes
|
265
276
|
composite_fks.each{|fk| send(:foreign_key, fk[:columns], fk)} if composite_fks
|
266
277
|
end
|
267
278
|
end
|
@@ -276,15 +287,21 @@ END_MIG
|
|
276
287
|
end
|
277
288
|
im = method(:index_to_generator_opts)
|
278
289
|
gen = Schema::Generator.new(self) do
|
279
|
-
indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts))}
|
290
|
+
indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))}
|
280
291
|
end
|
281
292
|
gen.dump_indexes(meth=>table, :ignore_errors=>!options[:same_db])
|
282
293
|
end
|
283
294
|
|
284
295
|
# Convert the parsed index information into options to the Generators index method.
|
285
|
-
def index_to_generator_opts(table, name, index_opts)
|
296
|
+
def index_to_generator_opts(table, name, index_opts, options={})
|
286
297
|
h = {}
|
287
|
-
|
298
|
+
if options[:index_names] != false && default_index_name(table, index_opts[:columns]) != name.to_s
|
299
|
+
if options[:index_names] == :namespace && !global_index_namespace?
|
300
|
+
h[:name] = "#{table}_#{name}".to_sym
|
301
|
+
else
|
302
|
+
h[:name] = name
|
303
|
+
end
|
304
|
+
end
|
288
305
|
h[:unique] = true if index_opts[:unique]
|
289
306
|
h
|
290
307
|
end
|
@@ -444,6 +461,7 @@ END_MIG
|
|
444
461
|
"index #{cols.inspect}#{opts_inspect(c)}"
|
445
462
|
end
|
446
463
|
end
|
464
|
+
is = is.reverse if options[:drop_index]
|
447
465
|
is.join("\n")
|
448
466
|
end
|
449
467
|
|
@@ -1644,32 +1644,36 @@ module Sequel
|
|
1644
1644
|
end
|
1645
1645
|
end
|
1646
1646
|
|
1647
|
-
# Set the given object as the associated object for the given
|
1648
|
-
def
|
1649
|
-
|
1647
|
+
# Set the given object as the associated object for the given *_to_one association reflection
|
1648
|
+
def _set_associated_object(opts, o)
|
1649
|
+
a = associations[opts[:name]]
|
1650
|
+
return if a && a == o && !set_associated_object_if_same?
|
1650
1651
|
run_association_callbacks(opts, :before_set, o)
|
1651
|
-
|
1652
|
-
remove_reciprocal_object(opts, a)
|
1653
|
-
end
|
1652
|
+
remove_reciprocal_object(opts, a) if a
|
1654
1653
|
send(opts._setter_method, o)
|
1655
1654
|
associations[opts[:name]] = o
|
1656
1655
|
add_reciprocal_object(opts, o) if o
|
1657
1656
|
run_association_callbacks(opts, :after_set, o)
|
1658
1657
|
o
|
1659
1658
|
end
|
1659
|
+
|
1660
|
+
# Whether run the associated object setter code if passed the same object as the one already
|
1661
|
+
# cached in the association. Usually not set (so nil), can be set on a per-object basis
|
1662
|
+
# if necessary.
|
1663
|
+
def set_associated_object_if_same?
|
1664
|
+
@set_associated_object_if_same
|
1665
|
+
end
|
1660
1666
|
|
1667
|
+
# Set the given object as the associated object for the given many_to_one association reflection
|
1668
|
+
def set_associated_object(opts, o)
|
1669
|
+
raise(Error, "associated object #{o.inspect} does not have a primary key") if o && !o.pk
|
1670
|
+
_set_associated_object(opts, o)
|
1671
|
+
end
|
1672
|
+
|
1661
1673
|
# Set the given object as the associated object for the given one_to_one association reflection
|
1662
1674
|
def set_one_to_one_associated_object(opts, o)
|
1663
1675
|
raise(Error, "object #{inspect} does not have a primary key") unless pk
|
1664
|
-
|
1665
|
-
if a = associations[opts[:name]]
|
1666
|
-
remove_reciprocal_object(opts, a)
|
1667
|
-
end
|
1668
|
-
send(opts._setter_method, o)
|
1669
|
-
associations[opts[:name]] = o
|
1670
|
-
add_reciprocal_object(opts, o) if o
|
1671
|
-
run_association_callbacks(opts, :after_set, o)
|
1672
|
-
o
|
1676
|
+
_set_associated_object(opts, o)
|
1673
1677
|
end
|
1674
1678
|
end
|
1675
1679
|
|
data/lib/sequel/model/base.rb
CHANGED
@@ -350,16 +350,14 @@ module Sequel
|
|
350
350
|
subclass.instance_variable_set(iv, sup_class_value)
|
351
351
|
end
|
352
352
|
unless ivs.include?("@dataset")
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
subclass.set_dataset(subclass.implicit_table_name)
|
358
|
-
elsif @dataset
|
359
|
-
subclass.set_dataset(@dataset.clone, :inherited=>true)
|
353
|
+
if self == Model || !@dataset
|
354
|
+
n = subclass.name
|
355
|
+
unless n.nil? || n.empty?
|
356
|
+
db
|
357
|
+
subclass.set_dataset(subclass.implicit_table_name) rescue nil
|
360
358
|
end
|
361
|
-
|
362
|
-
nil
|
359
|
+
elsif @dataset
|
360
|
+
subclass.set_dataset(@dataset.clone, :inherited=>true) rescue nil
|
363
361
|
end
|
364
362
|
end
|
365
363
|
end
|
@@ -1106,7 +1104,8 @@ module Sequel
|
|
1106
1104
|
# a.update(...)
|
1107
1105
|
# end
|
1108
1106
|
def lock!
|
1109
|
-
|
1107
|
+
_refresh(this.for_update) unless new?
|
1108
|
+
self
|
1110
1109
|
end
|
1111
1110
|
|
1112
1111
|
# Remove elements of the model object that make marshalling fail. Returns self.
|
@@ -1185,6 +1184,7 @@ module Sequel
|
|
1185
1184
|
def refresh
|
1186
1185
|
raise Sequel::Error, "can't refresh frozen object" if frozen?
|
1187
1186
|
_refresh(this)
|
1187
|
+
self
|
1188
1188
|
end
|
1189
1189
|
|
1190
1190
|
# Alias of refresh, but not aliased directly to make overriding in a plugin easier.
|
@@ -1532,7 +1532,6 @@ module Sequel
|
|
1532
1532
|
def _refresh(dataset)
|
1533
1533
|
set_values(_refresh_get(dataset) || raise(Error, "Record not found"))
|
1534
1534
|
changed_columns.clear
|
1535
|
-
self
|
1536
1535
|
end
|
1537
1536
|
|
1538
1537
|
# Get the row of column data from the database.
|
@@ -1809,7 +1808,7 @@ module Sequel
|
|
1809
1808
|
# for database specific column types.
|
1810
1809
|
def typecast_value(column, value)
|
1811
1810
|
return value unless typecast_on_assignment && db_schema && (col_schema = db_schema[column])
|
1812
|
-
value = nil if
|
1811
|
+
value = nil if '' == value and typecast_empty_string_to_nil and col_schema[:type] and ![:string, :blob].include?(col_schema[:type])
|
1813
1812
|
raise(InvalidValue, "nil/NULL is not allowed for the #{column} column") if raise_on_typecast_failure && value.nil? && (col_schema[:allow_null] == false)
|
1814
1813
|
begin
|
1815
1814
|
model.db.typecast_value(col_schema[:type], value)
|
@@ -150,9 +150,8 @@ module Sequel
|
|
150
150
|
module InstanceMethods
|
151
151
|
# Clear the cached compositions when refreshing.
|
152
152
|
def _refresh(ds)
|
153
|
-
|
153
|
+
super
|
154
154
|
compositions.clear
|
155
|
-
v
|
156
155
|
end
|
157
156
|
|
158
157
|
# For each composition, set the columns in the model class based
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The eager_each plugin makes calling each on an eager loaded dataset do eager loading.
|
4
|
+
# By default, each does not work on an eager loaded dataset, because each iterates
|
5
|
+
# over rows of the dataset as they come in, and to eagerly load you need to have all
|
6
|
+
# values up front. With the default associations code, you must call #all on an eagerly
|
7
|
+
# loaded dataset, as calling #each on an #eager dataset skips the eager loading, and calling
|
8
|
+
# #each on an #eager_graph dataset makes it yield plain hashes with columns from all
|
9
|
+
# tables, instead of yielding the instances of the main model.
|
10
|
+
#
|
11
|
+
# This plugin makes #each call #all for eagerly loaded datasets. As #all usually calls
|
12
|
+
# #each, this is a bit of issue, but this plugin resolves the issue by cloning the dataset
|
13
|
+
# and setting a new flag in the cloned dataset, so that each can check with the flag to
|
14
|
+
# determine whether it should call all.
|
15
|
+
#
|
16
|
+
# Usage:
|
17
|
+
#
|
18
|
+
# # Make all model subclass instances eagerly load for each (called before loading subclasses)
|
19
|
+
# Sequel::Model.plugin :eager_each
|
20
|
+
#
|
21
|
+
# # Make the Album class eagerly load for each
|
22
|
+
# Album.plugin :eager_each
|
23
|
+
module EagerEach
|
24
|
+
# Methods added to eagerly loaded datasets when the eager_each plugin is in use.
|
25
|
+
module EagerDatasetMethods
|
26
|
+
# Call #all instead of #each unless #each is being called by #all.
|
27
|
+
def each(&block)
|
28
|
+
if opts[:all_called]
|
29
|
+
super
|
30
|
+
else
|
31
|
+
all(&block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Clone the dataset and set a flag to let #each know not to call #all,
|
36
|
+
# to avoid the infinite loop.
|
37
|
+
def all(&block)
|
38
|
+
if opts[:all_called]
|
39
|
+
super
|
40
|
+
else
|
41
|
+
clone(:all_called=>true).all(&block)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module DatasetMethods
|
47
|
+
# Make sure calling each on this dataset will eagerly load the dataset.
|
48
|
+
def eager(*)
|
49
|
+
super.extend(EagerDatasetMethods)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Make sure calling each on this dataset will eagerly load the dataset.
|
53
|
+
def eager_graph(*)
|
54
|
+
super.extend(EagerDatasetMethods)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -139,8 +139,13 @@ module Sequel
|
|
139
139
|
module InstanceMethods
|
140
140
|
# Parse the provided JSON, which should return a hash,
|
141
141
|
# and call +set+ with that hash.
|
142
|
-
def from_json(json)
|
143
|
-
|
142
|
+
def from_json(json, opts={})
|
143
|
+
h = JSON.parse(json)
|
144
|
+
if fields = opts[:fields]
|
145
|
+
set_fields(h, fields, opts)
|
146
|
+
else
|
147
|
+
set(h)
|
148
|
+
end
|
144
149
|
end
|
145
150
|
|
146
151
|
# Return a string in JSON format. Accepts the following
|
@@ -200,7 +205,21 @@ module Sequel
|
|
200
205
|
module DatasetMethods
|
201
206
|
# Return a JSON string representing an array of all objects in
|
202
207
|
# this dataset. Takes the same options as the the instance
|
203
|
-
# method, and passes them to every instance.
|
208
|
+
# method, and passes them to every instance. Additionally,
|
209
|
+
# respects the following options:
|
210
|
+
#
|
211
|
+
# :array :: An array of instances. If this is not provided,
|
212
|
+
# calls #all on the receiver to get the array.
|
213
|
+
# :root :: If set to :collection, only wraps the collection
|
214
|
+
# in a root object. If set to :instance, only wraps
|
215
|
+
# the instances in a root object. If set to :both,
|
216
|
+
# wraps both the collection and instances in a root
|
217
|
+
# object. Unfortunately, for backwards compatibility,
|
218
|
+
# if this option is true and doesn't match one of those
|
219
|
+
# symbols, it defaults to both. That may change in a
|
220
|
+
# future version, so for forwards compatibility, you
|
221
|
+
# should pick a specific symbol for your desired
|
222
|
+
# behavior.
|
204
223
|
def to_json(*a)
|
205
224
|
if opts = a.first.is_a?(Hash)
|
206
225
|
opts = model.json_serializer_opts.merge(a.first)
|
@@ -208,6 +227,19 @@ module Sequel
|
|
208
227
|
else
|
209
228
|
opts = model.json_serializer_opts
|
210
229
|
end
|
230
|
+
|
231
|
+
collection_root = case opts[:root]
|
232
|
+
when nil, false, :instance
|
233
|
+
false
|
234
|
+
when :collection
|
235
|
+
opts = opts.dup
|
236
|
+
opts.delete(:root)
|
237
|
+
opts[:naked] = true unless opts.has_key?(:naked)
|
238
|
+
true
|
239
|
+
else
|
240
|
+
true
|
241
|
+
end
|
242
|
+
|
211
243
|
res = if row_proc
|
212
244
|
array = if opts[:array]
|
213
245
|
opts = opts.dup
|
@@ -219,7 +251,12 @@ module Sequel
|
|
219
251
|
else
|
220
252
|
all
|
221
253
|
end
|
222
|
-
|
254
|
+
|
255
|
+
if collection_root
|
256
|
+
{model.send(:pluralize, model.send(:underscore, model.to_s)) => res}.to_json(*a)
|
257
|
+
else
|
258
|
+
res.to_json(*a)
|
259
|
+
end
|
223
260
|
end
|
224
261
|
end
|
225
262
|
end
|