sequel 3.35.0 → 3.36.0

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.
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