From 87e917423d8c5cd67026982951413031f074bade Mon Sep 17 00:00:00 2001 From: Jeremy Woertink Date: Sat, 23 Mar 2024 10:26:24 -0700 Subject: [PATCH] Adds new JSON::Any criteria methods for querying jsonb columns that contain the ? character. Ref #197 --- spec/avram/json_criteria_spec.cr | 45 ++++++++++++++++++ src/avram/charms/json_extensions.cr | 14 ++++++ src/avram/where.cr | 72 +++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 spec/avram/json_criteria_spec.cr diff --git a/spec/avram/json_criteria_spec.cr b/spec/avram/json_criteria_spec.cr new file mode 100644 index 000000000..5e8b09cb4 --- /dev/null +++ b/spec/avram/json_criteria_spec.cr @@ -0,0 +1,45 @@ +require "../spec_helper" + +private class QueryMe < BaseModel + COLUMN_SQL = "users.id, users.created_at, users.updated_at, users.preferences" + + table users do + column preferences : JSON::Any + end +end + +describe JSON::Any::Lucky::Criteria do + describe "has_key" do + it "?" do + preferences.has_key("theme").to_sql.should eq ["SELECT #{QueryMe::COLUMN_SQL} FROM users WHERE users.preferences ? $1", "theme"] + end + + it "negates with NOT()" do + preferences.not.has_key("theme").to_sql.should eq ["SELECT #{QueryMe::COLUMN_SQL} FROM users WHERE NOT(users.preferences ? $1)", "theme"] + end + end + + describe "has_any_keys" do + it "?|" do + preferences.has_any_keys(["theme", "style"]).to_sql.should eq ["SELECT #{QueryMe::COLUMN_SQL} FROM users WHERE users.preferences ?| $1", ["theme", "style"]] + end + + it "negates with NOT()" do + preferences.not.has_any_keys(["theme", "style"]).to_sql.should eq ["SELECT #{QueryMe::COLUMN_SQL} FROM users WHERE NOT(users.preferences ?| $1)", ["theme", "style"]] + end + end + + describe "has_all_keys" do + it "?&" do + preferences.has_all_keys(["theme", "style"]).to_sql.should eq ["SELECT #{QueryMe::COLUMN_SQL} FROM users WHERE users.preferences ?& $1", ["theme", "style"]] + end + + it "negates with NOT()" do + preferences.not.has_all_keys(["theme", "style"]).to_sql.should eq ["SELECT #{QueryMe::COLUMN_SQL} FROM users WHERE NOT(users.preferences ?& $1)", ["theme", "style"]] + end + end +end + +private def preferences + QueryMe::BaseQuery.new.preferences +end diff --git a/src/avram/charms/json_extensions.cr b/src/avram/charms/json_extensions.cr index 4e1a33ca2..d260ecbb3 100644 --- a/src/avram/charms/json_extensions.cr +++ b/src/avram/charms/json_extensions.cr @@ -76,6 +76,20 @@ struct JSON::Any end class Criteria(T, V) < Avram::Criteria(T, V) + # performs `WHERE jsonb ? string` + def has_key(value : String) : T + add_clause(Avram::Where::JSONHasKey.new(column, value)) + end + + # performs `WHERE jsonb ?| array` + def has_any_keys(keys : Array(String)) : T + add_clause(Avram::Where::JSONHasAnyKeys.new(column, keys)) + end + + # performs `WHERE jsonb ?& array` + def has_all_keys(keys : Array(String)) : T + add_clause(Avram::Where::JSONHasAllKeys.new(column, keys)) + end end end end diff --git a/src/avram/where.cr b/src/avram/where.cr index 0c249c07b..2c121d7c6 100644 --- a/src/avram/where.cr +++ b/src/avram/where.cr @@ -245,6 +245,78 @@ module Avram::Where end end + class JSONHasKey < ValueHoldingSqlClause + def operator : String + "?" + end + + def negated : NotJSONHasKey + NotJSONHasKey.new(column, value) + end + end + + class NotJSONHasKey < ValueHoldingSqlClause + def operator : String + "?" + end + + def negated : JSONHasKey + JSONHasKey.new(column, value) + end + + def prepare(placeholder_supplier : Proc(String)) : String + "NOT(#{column} #{operator} #{placeholder_supplier.call})" + end + end + + class JSONHasAnyKeys < ValueHoldingSqlClause + def operator : String + "?|" + end + + def negated : NotJSONHasAnyKeys + NotJSONHasAnyKeys.new(column, value) + end + end + + class NotJSONHasAnyKeys < ValueHoldingSqlClause + def operator : String + "?|" + end + + def negated : JSONHasAnyKeys + JSONHasAnyKeys.new(column, value) + end + + def prepare(placeholder_supplier : Proc(String)) : String + "NOT(#{column} #{operator} #{placeholder_supplier.call})" + end + end + + class JSONHasAllKeys < ValueHoldingSqlClause + def operator : String + "?&" + end + + def negated : NotJSONHasAllKeys + NotJSONHasAllKeys.new(column, value) + end + end + + class NotJSONHasAllKeys < ValueHoldingSqlClause + def operator : String + "?&" + end + + def negated : JSONHasAllKeys + JSONHasAllKeys.new(column, value) + end + + def prepare(placeholder_supplier : Proc(String)) : String + "NOT(#{column} #{operator} #{placeholder_supplier.call})" + end + end + class Raw < Condition @clause : String