Skip to content

Commit

Permalink
Re-add is_json and is_not_json methods to the pg_json_ops extension, …
Browse files Browse the repository at this point in the history
…as the support was re-added in PostgreSQL 16

This reverts commit 98f8c9b. It updates
the documentation and guard to use PostgreSQL 16 instead of 15, and fixes
some documentation issues.
  • Loading branch information
jeremyevans committed Oct 6, 2023
1 parent 813db79 commit 01613aa
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
=== master

* Re-add is_json and is_not_json methods to the pg_json_ops extension, as the support was re-added in PostgreSQL 16 (jeremyevans)

* Avoid infinite loop when handling exceptions with a cause loop in jdbc adapter (jeremyevans)

=== 5.73.0 (2023-10-01)
Expand Down
52 changes: 52 additions & 0 deletions lib/sequel/extensions/pg_json_ops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@
# c = Sequel.pg_jsonb_op(:c)
# DB[:t].update(c['key1'] => 1.to_json, c['key2'] => "a".to_json)
#
# On PostgreSQL 16+, the <tt>IS [NOT] JSON</tt> operator is supported:
#
# j.is_json # j IS JSON
# j.is_json(type: :object) # j IS JSON OBJECT
# j.is_json(type: :object, unique: true) # j IS JSON OBJECT WITH UNIQUE
# j.is_not_json # j IS NOT JSON
# j.is_not_json(type: :array) # j IS NOT JSON ARRAY
# j.is_not_json(unique: true) # j IS NOT JSON WITH UNIQUE
#
# If you are also using the pg_json extension, you should load it before
# loading this extension. Doing so will allow you to use the #op method on
# JSONHash, JSONHarray, JSONBHash, and JSONBArray, allowing you to perform json/jsonb operations
Expand Down Expand Up @@ -151,6 +160,18 @@ class JSONBaseOp < Sequel::SQL::Wrapper
GET_PATH = ["(".freeze, " #> ".freeze, ")".freeze].freeze
GET_PATH_TEXT = ["(".freeze, " #>> ".freeze, ")".freeze].freeze

IS_JSON = ["(".freeze, " IS JSON".freeze, "".freeze, ")".freeze].freeze
IS_NOT_JSON = ["(".freeze, " IS NOT JSON".freeze, "".freeze, ")".freeze].freeze
EMPTY_STRING = Sequel::LiteralString.new('').freeze
WITH_UNIQUE = Sequel::LiteralString.new(' WITH UNIQUE').freeze
IS_JSON_MAP = {
nil => EMPTY_STRING,
:value => Sequel::LiteralString.new(' VALUE').freeze,
:scalar => Sequel::LiteralString.new(' SCALAR').freeze,
:object => Sequel::LiteralString.new(' OBJECT').freeze,
:array => Sequel::LiteralString.new(' ARRAY').freeze
}.freeze

# Get JSON array element or object field as json. If an array is given,
# gets the object at the specified path.
#
Expand Down Expand Up @@ -233,6 +254,30 @@ def get_text(key)
end
end

# Return whether the json object can be parsed as JSON.
#
# Options:
# :type :: Check whether the json object can be parsed as a specific type
# of JSON (:value, :scalar, :object, :array).
# :unique :: Check JSON objects for unique keys.
#
# json_op.is_json # json IS JSON
# json_op.is_json(type: :object) # json IS JSON OBJECT
# json_op.is_json(unique: true) # json IS JSON WITH UNIQUE
def is_json(opts=OPTS)
_is_json(IS_JSON, opts)
end

# Return whether the json object cannot be parsed as JSON. The opposite
# of #is_json. See #is_json for options.
#
# json_op.is_not_json # json IS NOT JSON
# json_op.is_not_json(type: :object) # json IS NOT JSON OBJECT
# json_op.is_not_json(unique: true) # json IS NOT JSON WITH UNIQUE
def is_not_json(opts=OPTS)
_is_json(IS_NOT_JSON, opts)
end

# Returns a set of keys AS text in the json object.
#
# json_op.keys # json_object_keys(json)
Expand Down Expand Up @@ -286,6 +331,13 @@ def typeof

private

# Internals of IS [NOT] JSON support
def _is_json(lit_array, opts)
raise Error, "invalid is_json :type option: #{opts[:type].inspect}" unless type = IS_JSON_MAP[opts[:type]]
unique = opts[:unique] ? WITH_UNIQUE : EMPTY_STRING
Sequel::SQL::BooleanExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(lit_array, [self, type, unique]))
end

# Return a placeholder literal with the given str and args, wrapped
# in an JSONOp or JSONBOp, used by operators that return json or jsonb.
def json_op(str, args)
Expand Down
39 changes: 39 additions & 0 deletions spec/adapters/postgres_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4177,6 +4177,45 @@ def left_item_id
Sequel.pg_jsonb_op(Sequel[:i])['d'] => Sequel.pg_jsonb('e'=>4))
@db[:items].all.must_equal [{:i=>{'a'=>{'b'=>2, 'c'=>3}, 'd'=>{'e'=>4}}}]
end if DB.server_version >= 140000 && json_type == :jsonb

it "15 #{json_type} operations/functions with pg_json_ops" do
meth = Sequel.method(:"pg_#{json_type}_op")
@db.get(meth.call('{}').is_json).must_equal true
@db.get(meth.call('null').is_json).must_equal true
@db.get(meth.call('1').is_json).must_equal true
@db.get(meth.call('"a"').is_json).must_equal true
@db.get(meth.call('[]').is_json).must_equal true
@db.get(meth.call('').is_json).must_equal false

@db.get(meth.call('1').is_json(:type=>:scalar)).must_equal true
@db.get(meth.call('null').is_json(:type=>:value)).must_equal true
@db.get(meth.call('{}').is_json(:type=>:object)).must_equal true
@db.get(meth.call('{}').is_json(:type=>:array)).must_equal false
@db.get(meth.call('{"a": 1, "a": 2}').is_json(:type=>:object, :unique=>true)).must_equal false
@db.get(meth.call('{"a": 1, "b": 2}').is_json(:type=>:object, :unique=>true)).must_equal true
@db.get(meth.call('[]').is_json(:type=>:object, :unique=>true)).must_equal false
@db.get(meth.call('{"a": 1, "a": 2}').is_json(:unique=>true)).must_equal false
@db.get(meth.call('{"a": 1, "b": 2}').is_json(:unique=>true)).must_equal true
@db.get(meth.call('[]').is_json(:unique=>true)).must_equal true

@db.get(meth.call('{}').is_not_json).must_equal false
@db.get(meth.call('null').is_not_json).must_equal false
@db.get(meth.call('1').is_not_json).must_equal false
@db.get(meth.call('"a"').is_not_json).must_equal false
@db.get(meth.call('[]').is_not_json).must_equal false
@db.get(meth.call('').is_not_json).must_equal true

@db.get(meth.call('1').is_not_json(:type=>:scalar)).must_equal false
@db.get(meth.call('null').is_not_json(:type=>:value)).must_equal false
@db.get(meth.call('{}').is_not_json(:type=>:object)).must_equal false
@db.get(meth.call('{}').is_not_json(:type=>:array)).must_equal true
@db.get(meth.call('{"a": 1, "a": 2}').is_not_json(:type=>:object, :unique=>true)).must_equal true
@db.get(meth.call('{"a": 1, "b": 2}').is_not_json(:type=>:object, :unique=>true)).must_equal false
@db.get(meth.call('[]').is_not_json(:type=>:object, :unique=>true)).must_equal true
@db.get(meth.call('{"a": 1, "a": 2}').is_not_json(:unique=>true)).must_equal true
@db.get(meth.call('{"a": 1, "b": 2}').is_not_json(:unique=>true)).must_equal false
@db.get(meth.call('[]').is_not_json(:unique=>true)).must_equal false
end if DB.server_version >= 160000
end
end if DB.server_version >= 90200

Expand Down
73 changes: 73 additions & 0 deletions spec/extensions/pg_json_ops_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,79 @@ def @db.server_version(*); 130000; end
@l[@jb.extract_text('a') + 'a'].must_equal "(jsonb_extract_path_text(j, 'a') || 'a')"
end

it "should have #is_json work without arguments" do
@l[@j.is_json].must_equal "(j IS JSON)"
@l[@jb.is_json].must_equal "(j IS JSON)"
end

it "should have #is_json respect :type option" do
[@j, @jb].each do |j|
@l[j.is_json(:type=>:value)].must_equal "(j IS JSON VALUE)"
@l[j.is_json(:type=>:scalar)].must_equal "(j IS JSON SCALAR)"
@l[j.is_json(:type=>:object)].must_equal "(j IS JSON OBJECT)"
@l[j.is_json(:type=>:array)].must_equal "(j IS JSON ARRAY)"
end
end

it "should have #is_json respect :unique option" do
@l[@j.is_json(:unique=>true)].must_equal "(j IS JSON WITH UNIQUE)"
@l[@jb.is_json(:unique=>true)].must_equal "(j IS JSON WITH UNIQUE)"
end

it "should have #is_json respect :type and :unique options" do
[@j, @jb].each do |j|
@l[j.is_json(:type=>:value, :unique=>true)].must_equal "(j IS JSON VALUE WITH UNIQUE)"
@l[j.is_json(:type=>:scalar, :unique=>true)].must_equal "(j IS JSON SCALAR WITH UNIQUE)"
@l[j.is_json(:type=>:object, :unique=>true)].must_equal "(j IS JSON OBJECT WITH UNIQUE)"
@l[j.is_json(:type=>:array, :unique=>true)].must_equal "(j IS JSON ARRAY WITH UNIQUE)"
end
end

it "should have #is_json return an SQL::BooleanExpression" do
@l[~@j.is_json].must_equal "NOT (j IS JSON)"
@l[~@jb.is_json].must_equal "NOT (j IS JSON)"
end

it "should have #is_not_json work without arguments" do
@l[@j.is_not_json].must_equal "(j IS NOT JSON)"
@l[@jb.is_not_json].must_equal "(j IS NOT JSON)"
end

it "should have #is_not_json respect :type option" do
[@j, @jb].each do |j|
@l[j.is_not_json(:type=>:value)].must_equal "(j IS NOT JSON VALUE)"
@l[j.is_not_json(:type=>:scalar)].must_equal "(j IS NOT JSON SCALAR)"
@l[j.is_not_json(:type=>:object)].must_equal "(j IS NOT JSON OBJECT)"
@l[j.is_not_json(:type=>:array)].must_equal "(j IS NOT JSON ARRAY)"
end
end

it "should have #is_not_json respect :unique option" do
@l[@j.is_not_json(:unique=>true)].must_equal "(j IS NOT JSON WITH UNIQUE)"
@l[@jb.is_not_json(:unique=>true)].must_equal "(j IS NOT JSON WITH UNIQUE)"
end

it "should have #is_not_json respect :type and :unique options" do
[@j, @jb].each do |j|
@l[j.is_not_json(:type=>:value, :unique=>true)].must_equal "(j IS NOT JSON VALUE WITH UNIQUE)"
@l[j.is_not_json(:type=>:scalar, :unique=>true)].must_equal "(j IS NOT JSON SCALAR WITH UNIQUE)"
@l[j.is_not_json(:type=>:object, :unique=>true)].must_equal "(j IS NOT JSON OBJECT WITH UNIQUE)"
@l[j.is_not_json(:type=>:array, :unique=>true)].must_equal "(j IS NOT JSON ARRAY WITH UNIQUE)"
end
end

it "should have #is_not_json return an SQL::BooleanExpression" do
@l[~@j.is_not_json].must_equal "NOT (j IS NOT JSON)"
@l[~@jb.is_not_json].must_equal "NOT (j IS NOT JSON)"
end

it "should have #is_json and #is_not_json raise for invalid :type" do
proc{@j.is_json(:type=>:foo)}.must_raise Sequel::Error
proc{@jb.is_json(:type=>:foo)}.must_raise Sequel::Error
proc{@j.is_not_json(:type=>:foo)}.must_raise Sequel::Error
proc{@jb.is_not_json(:type=>:foo)}.must_raise Sequel::Error
end

it "should have #keys use the json_object_keys function" do
@l[@j.keys].must_equal "json_object_keys(j)"
@l[@jb.keys].must_equal "jsonb_object_keys(j)"
Expand Down

0 comments on commit 01613aa

Please sign in to comment.