sequel 3.35.0 → 3.36.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/CHANGELOG +78 -0
  2. data/Rakefile +3 -3
  3. data/bin/sequel +3 -1
  4. data/doc/advanced_associations.rdoc +154 -11
  5. data/doc/migration.rdoc +18 -0
  6. data/doc/object_model.rdoc +541 -0
  7. data/doc/opening_databases.rdoc +4 -1
  8. data/doc/release_notes/3.36.0.txt +245 -0
  9. data/doc/schema_modification.rdoc +0 -6
  10. data/lib/sequel/adapters/do/mysql.rb +7 -0
  11. data/lib/sequel/adapters/jdbc.rb +11 -3
  12. data/lib/sequel/adapters/jdbc/mysql.rb +3 -5
  13. data/lib/sequel/adapters/jdbc/postgresql.rb +10 -8
  14. data/lib/sequel/adapters/jdbc/progress.rb +21 -0
  15. data/lib/sequel/adapters/mock.rb +2 -6
  16. data/lib/sequel/adapters/mysql.rb +3 -9
  17. data/lib/sequel/adapters/mysql2.rb +12 -11
  18. data/lib/sequel/adapters/postgres.rb +32 -40
  19. data/lib/sequel/adapters/shared/mssql.rb +15 -11
  20. data/lib/sequel/adapters/shared/mysql.rb +28 -3
  21. data/lib/sequel/adapters/shared/oracle.rb +5 -0
  22. data/lib/sequel/adapters/shared/postgres.rb +59 -5
  23. data/lib/sequel/adapters/shared/sqlite.rb +3 -13
  24. data/lib/sequel/adapters/sqlite.rb +0 -7
  25. data/lib/sequel/adapters/swift/mysql.rb +2 -5
  26. data/lib/sequel/adapters/tinytds.rb +1 -2
  27. data/lib/sequel/connection_pool/sharded_threaded.rb +5 -1
  28. data/lib/sequel/connection_pool/threaded.rb +9 -1
  29. data/lib/sequel/database/dataset_defaults.rb +3 -1
  30. data/lib/sequel/database/misc.rb +7 -1
  31. data/lib/sequel/database/query.rb +11 -3
  32. data/lib/sequel/database/schema_generator.rb +40 -9
  33. data/lib/sequel/database/schema_methods.rb +6 -1
  34. data/lib/sequel/dataset/actions.rb +5 -5
  35. data/lib/sequel/dataset/prepared_statements.rb +3 -1
  36. data/lib/sequel/dataset/query.rb +1 -1
  37. data/lib/sequel/extensions/migration.rb +28 -0
  38. data/lib/sequel/extensions/pg_auto_parameterize.rb +0 -9
  39. data/lib/sequel/extensions/pg_inet.rb +89 -0
  40. data/lib/sequel/extensions/pg_json.rb +178 -0
  41. data/lib/sequel/extensions/schema_dumper.rb +24 -6
  42. data/lib/sequel/model/associations.rb +19 -15
  43. data/lib/sequel/model/base.rb +11 -12
  44. data/lib/sequel/plugins/composition.rb +1 -2
  45. data/lib/sequel/plugins/eager_each.rb +59 -0
  46. data/lib/sequel/plugins/json_serializer.rb +41 -4
  47. data/lib/sequel/plugins/nested_attributes.rb +72 -52
  48. data/lib/sequel/plugins/optimistic_locking.rb +8 -0
  49. data/lib/sequel/plugins/tactical_eager_loading.rb +7 -7
  50. data/lib/sequel/version.rb +1 -1
  51. data/spec/adapters/postgres_spec.rb +271 -1
  52. data/spec/adapters/sqlite_spec.rb +11 -0
  53. data/spec/core/connection_pool_spec.rb +26 -1
  54. data/spec/core/database_spec.rb +19 -0
  55. data/spec/core/dataset_spec.rb +45 -5
  56. data/spec/core/expression_filters_spec.rb +31 -67
  57. data/spec/core/mock_adapter_spec.rb +4 -0
  58. data/spec/extensions/core_extensions_spec.rb +83 -0
  59. data/spec/extensions/eager_each_spec.rb +34 -0
  60. data/spec/extensions/inflector_spec.rb +0 -4
  61. data/spec/extensions/json_serializer_spec.rb +32 -1
  62. data/spec/extensions/migration_spec.rb +28 -0
  63. data/spec/extensions/nested_attributes_spec.rb +134 -1
  64. data/spec/extensions/optimistic_locking_spec.rb +15 -1
  65. data/spec/extensions/pg_hstore_spec.rb +1 -1
  66. data/spec/extensions/pg_inet_spec.rb +44 -0
  67. data/spec/extensions/pg_json_spec.rb +101 -0
  68. data/spec/extensions/prepared_statements_spec.rb +30 -0
  69. data/spec/extensions/rcte_tree_spec.rb +9 -0
  70. data/spec/extensions/schema_dumper_spec.rb +195 -7
  71. data/spec/extensions/serialization_spec.rb +4 -0
  72. data/spec/extensions/spec_helper.rb +9 -1
  73. data/spec/extensions/tactical_eager_loading_spec.rb +8 -0
  74. data/spec/integration/database_test.rb +5 -1
  75. data/spec/integration/prepared_statement_test.rb +20 -2
  76. data/spec/model/associations_spec.rb +27 -0
  77. data/spec/model/base_spec.rb +54 -0
  78. data/spec/model/model_spec.rb +6 -0
  79. data/spec/model/record_spec.rb +18 -0
  80. data/spec/rcov.opts +2 -0
  81. 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(?:medium|small)?int(?:eger)?(?:\((?:\d+)\))?(?: unsigned)?\z/o
148
- {:type=>Integer}
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
- h[:name] = name unless default_index_name(table, index_opts[:columns]) == name.to_s
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 many_to_one association reflection
1648
- def set_associated_object(opts, o)
1649
- raise(Error, "associated object #{o.inspect} does not have a primary key") if o && !o.pk
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
- if a = associations[opts[:name]]
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
- run_association_callbacks(opts, :before_set, o)
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
 
@@ -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
- db
354
- begin
355
- if self == Model || !@dataset
356
- n = subclass.name
357
- subclass.set_dataset(subclass.implicit_table_name) unless n.nil? || n.empty?
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
- rescue
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
- new? ? self : _refresh(this.for_update)
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 value == '' and typecast_empty_string_to_nil and col_schema[:type] and ![:string, :blob].include?(col_schema[:type])
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
- v = super
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
- set(JSON.parse(json))
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
- opts[:root] ? {model.send(:pluralize, model.send(:underscore, model.to_s)) => res}.to_json(*a) : res.to_json(*a)
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