require 'sequel/core' Sequel.extension(*%i(pg_json pg_json_ops)) module ROM module SQL module Postgres module Types JSONRead = (SQL::Types::Array | SQL::Types::Hash).constructor do |value| if value.respond_to?(:to_hash) value.to_hash elsif value.respond_to?(:to_ary) value.to_ary else value end end JSON = Type('json') do (SQL::Types::Array | SQL::Types::Hash) .constructor(Sequel.method(:pg_json)) .meta(read: JSONRead) end JSONB = Type('jsonb') do (SQL::Types::Array | SQL::Types::Hash) .constructor(Sequel.method(:pg_jsonb)) .meta(read: JSONRead) end # @!parse # class SQL::Attribute # # @!method contain(value) # # Check whether the JSON value includes a json value # # Translates to the @> operator # # # # @example # # people.where { fields.contain(gender: 'Female') } # # people.where(people[:fields].contain([name: 'age'])) # # people.select { fields.contain(gender: 'Female').as(:is_female) } # # # # @param [Hash,Array,Object] value # # # # @return [SQL::Attribute] # # # # @api public # # # @!method contained_by(value) # # Check whether the JSON value is contained by other value # # Translates to the <@ operator # # # # @example # # people.where { custom_values.contained_by(age: 25, foo: 'bar') } # # # # @param [Hash,Array] value # # # # @return [SQL::Attribute] # # # # @api public # # # @!method get(*path) # # Extract the JSON value using at the specified path # # Translates to -> or #> depending on the number of arguments # # # # @example # # people.select { data.get('age').as(:person_age) } # # people.select { fields.get(0).as(:first_field) } # # people.select { fields.get('0', 'value').as(:first_field_value) } # # # # @param [Array,Array] path Path to extract # # # # @return [SQL::Attribute,SQL::Attribute] # # # # @api public # # # @!method get_text(*path) # # Extract the JSON value as text using at the specified path # # Translates to ->> or #>> depending on the number of arguments # # # # @example # # people.select { data.get('age').as(:person_age) } # # people.select { fields.get(0).as(:first_field) } # # people.select { fields.get('0', 'value').as(:first_field_value) } # # # # @param [Array,Array] path Path to extract # # # # @return [SQL::Attribute] # # # # @api public # # # @!method has_key(key) # # Does the JSON value have the specified top-level key # # Translates to ? # # # # @example # # people.where { data.has_key('age') } # # # # @param [String] key # # # # @return [SQL::Attribute] # # # # @api public # # # @!method has_any_key(*keys) # # Does the JSON value have any of the specified top-level keys # # Translates to ?| # # # # @example # # people.where { data.has_any_key('age', 'height') } # # # # @param [Array] keys # # # # @return [SQL::Attribute] # # # # @api public # # # @!method has_all_keys(*keys) # # Does the JSON value have all the specified top-level keys # # Translates to ?& # # # # @example # # people.where { data.has_all_keys('age', 'height') } # # # # @param [Array] keys # # # # @return [SQL::Attribute] # # # # @api public # # # @!method merge(value) # # Concatenate two JSON values # # Translates to || # # # # @example # # people.select { data.merge(fetched_at: Time.now).as(:data) } # # people.select { (fields + [name: 'height', value: 165]).as(:fields) } # # # # @param [Hash,Array] value # # # # @return [SQL::Attribute] # # # # @api public # # # @!method +(value) # # An alias for SQL::Attribute#merge # # # # @api public # # # @!method delete(*path) # # Deletes the specified value by key, index, or path # # Translates to - or #- depending on the number of arguments # # # # @example # # people.select { data.delete('age').as(:data_without_age) } # # people.select { fields.delete(0).as(:fields_without_first) } # # people.select { fields.delete(-1).as(:fields_without_last) } # # people.select { data.delete('deeply', 'nested', 'value').as(:data) } # # people.select { fields.delete('0', 'name').as(:data) } # # # # @param [Array] path # # # # @return [SQL::Attribute] # # # # @api public # end module JSONMethods def self.[](type, wrap) parent = self Module.new do include parent define_method(:json_type) { type } define_method(:wrap, wrap) end end def get(type, expr, *path) Attribute[json_type].meta(sql_expr: wrap(expr)[path_args(path)]) end def get_text(type, expr, *path) Attribute[SQL::Types::String].meta(sql_expr: wrap(expr).get_text(path_args(path))) end private def path_args(path) case path.size when 0 then raise ArgumentError, "wrong number of arguments (given 0, expected 1+)" when 1 then path[0] else path end end end TypeExtensions.register(JSON) do include JSONMethods[JSON, :pg_json.to_proc] end TypeExtensions.register(JSONB) do include JSONMethods[JSONB, :pg_jsonb.to_proc] def contain(type, expr, value) Attribute[SQL::Types::Bool].meta(sql_expr: wrap(expr).contains(value)) end def contained_by(type, expr, value) Attribute[SQL::Types::Bool].meta(sql_expr: wrap(expr).contained_by(value)) end def has_key(type, expr, key) Attribute[SQL::Types::Bool].meta(sql_expr: wrap(expr).has_key?(key)) end def has_any_key(type, expr, *keys) Attribute[SQL::Types::Bool].meta(sql_expr: wrap(expr).contain_any(keys)) end def has_all_keys(type, expr, *keys) Attribute[SQL::Types::Bool].meta(sql_expr: wrap(expr).contain_all(keys)) end def merge(type, expr, value) Attribute[JSONB].meta(sql_expr: wrap(expr).concat(value)) end alias_method :+, :merge def delete(type, expr, *path) sql_expr = path.size == 1 ? wrap(expr) - path : wrap(expr).delete_path(path) Attribute[JSONB].meta(sql_expr: sql_expr) end end end end end end