Skip to content

Commit

Permalink
Add JSONB feature. Starting working on the documentation again
Browse files Browse the repository at this point in the history
  • Loading branch information
Yacine Petitprez committed Jun 30, 2018
1 parent 7d33a3a commit bf9740a
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 54 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
- SQL Builder can now be used with model collection as subqueries.
- Add methods for pagination (PR #16, thanks @jwoertink)
- Add multi-connections system (PR #18, thanks @russ)
- Add JSONB helpers
- Add JSONB helpers in expression engine. Check the manual
- Migrating the wiki to the sources of the project, to make easy to have PR for
updating the documentation !
- Add range support for `Sql::Node#in?` method:
Expand Down
52 changes: 49 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,62 @@
[![Build Status](https://travis-ci.org/anykeyh/clear.svg?branch=master)](https://travis-ci.org/anykeyh/clear) [![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://anykeyh.github.io/clear/) [![GitHub release](https://img.shields.io/github/release/anykeyh/clear.svg)](https://github.com/anykeyh/clear/releases)

Clear is an ORM built specifically for PostgreSQL in Crystal.
It follows Active Record design pattern, with emphasis on readability and
a coverage of postgres advanced features like jsonb.

It follows Active Record design pattern, with emphasis on readability and a
native coverage of Postgres advanced features like jsonb, hstore, ts-vector....

It is in active development and actively maintained !

## Why to use Clear ?

You want to use Clear if:

- [X] You want an expressive ORM. Put straight your thought to your code !
- [X] You'd like to use advanced Postgres features without hassle
- [X] You'd like to have your issue fixed in a day or two (we are active !)

You don't want to use Clear if:

- [ ] You need pure performance. Or you can use just the SQL builder API.
- [ ] You're obviously not on PostgreSQL
- [ ] You need something which doesn't evolve (e.g. production ready).
Clear is still in alpha (but starting to mature !).

## Features

- Active Record pattern based ORM
- Expressiveness as mantra - even with advanced features like jsonb, regexp... -
```crystal
# Like ...
Product.query.where{ ( type == "Book" ) & ( metadata.jsonb("author.full_name") == "Philip K. Dick" ) }
# ^--- will use @> operator, to relay on your gin index. For real.
Product.query.where{ ( products.type == "Book" ) & ( products.metadata.jsonb("author.full_name") != "Philip K. Dick" ) }
# ^--- this time will use ->> notation, because no optimizations possible :/
# Or...
User.query.where{ created_at.in? 5.days.ago .. 1.day.ago }
# Or even...
ORM.query.where{ ( description =~ /(^| )awesome($| )/i ) }.first!.name # Clear! :-)
```
- Proper debug information
- Log and colorize query. Show you the last query when your code crash !
- If failing on compile for a good reason, give proper explaination !
- Migration system
- Validation system
- N+1 query avoidance strategy
- Class polymorphism (table only, not in relations yet)
- Transaction, rollback, savepoint
- Access to CTE, locks, cursors, scope, pagination, join, window, multi-connection and many others features
- Advanced model lifecycle

## Getting started

- [Please check the manual which cover most of the library here](https://github.com/anykeyh/clear/tree/master/manual)
- [API Documentations](https://anykeyh.github.io/clear/)
- [Source Code](https://github.com/anykeyh/clear)
- [A simple example is available here](https://github.com/anykeyh/clear/blob/master/sample/wiki/getting_started.cr)

- [Changelog](https://github.com/anykeyh/clear/blob/master/CHANGELOG.md)


Expand Down
48 changes: 37 additions & 11 deletions spec/sql/jsonb_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ module JSONBSpec
end

it "can generate arrow writing" do
jsonb_resolve("data", "x.y.z").should eq("data->'x'->'y'->>'z'")
jsonb_resolve("data", "x.\\.y.z").should eq("data->'x'->'.y'->>'z'")
jsonb_resolve("data", "x.y'b.z").should eq("data->'x'->'y''b'->>'z'")
jsonb_resolve("data", "x.y.z").should eq("data->'x'->'y'->'z'")
jsonb_resolve("data", "x.\\.y.z").should eq("data->'x'->'.y'->'z'")
jsonb_resolve("data", "x.y'b.z", "text").should eq("data->'x'->'y''b'->'z'::text")
end

it "can use `?|` operator" do
Expand All @@ -45,23 +45,49 @@ module JSONBSpec
describe "Expression engine" do
it "use -> operator when it cannot test presence" do
Clear::SQL.select("*").from("users")
.where { data.jsonb("personal email").like "%@gmail.com" }.to_sql
.should eq %(SELECT * FROM users WHERE (data->>'personal email' LIKE '%@gmail.com'))
.where { data.jsonb("personal email").cast("text").like "%@gmail.com" }.to_sql
.should eq %(SELECT * FROM users WHERE (data->'personal email'::text LIKE '%@gmail.com'))

# v-- Usage of 'raw' should trigger usage of arrow, since it's not a literal.
# v-- Complex call
Clear::SQL.select.from("users")
.where { data.jsonb("test") == raw("afunction()") }.to_sql
.should eq %(SELECT * FROM users WHERE (data->>'test' = afunction()))
.where { data.jsonb("test") == call_function(data.jsonb("a.b.c")) }.to_sql
.should eq %(SELECT * FROM users WHERE (data->'test' = call_function(data->'a'->'b'->'c')))
end

it "merges the jsonb instructions (optimization)" do
it "uses @> operator when it can !" do
Clear::SQL.select("*").from("users")
.where {
(data.jsonb("security.role") == "admin") &
(data.jsonb("security.level") == 1)
}.to_sql
.should eq "SELECT * FROM users WHERE (data @> '{\"security\":{\"role\":\"admin\"}}') OR " +
"(data @> '{\"security\":{\"role\":1}}')"
.should eq "SELECT * FROM users WHERE (data @> '{\"security\":{\"role\":\"admin\"}}' AND " +
"data @> '{\"security\":{\"level\":1}}')"
end

it "check existence of a key" do
Clear::SQL.select.from("users")
.where { data.jsonb_key_exists?("test") }
.to_sql
.should eq "SELECT * FROM users WHERE (data ? 'test')"

Clear::SQL.select.from("users")
.where { data.jsonb("a").jsonb_key_exists?("test") }
.to_sql
.should eq "SELECT * FROM users WHERE (data->'a' ? 'test')"
end

it "check existence of any key" do
Clear::SQL.select.from("users")
.where { data.jsonb_any_key_exists?(["a", 0]) }
.to_sql
.should eq "SELECT * FROM users WHERE (data ?| array['a', 0])"
end

it "check existence of all keys" do
Clear::SQL.select.from("users")
.where { data.jsonb("a").jsonb_all_keys_exists?(["a", "b"]) }
.to_sql
.should eq "SELECT * FROM users WHERE (data->'a' ?& array['a', 'b'])"
end
end
end
Expand Down
16 changes: 12 additions & 4 deletions src/clear/expression/expression.cr
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ class Clear::Expression

# fastest way to call self.safe_literal
# See `safe_literal(x : _)`
def self.[](*args) : String
safe_literal(*args)
def self.[](arg)
safe_literal(arg)
end

def self.safe_literal(x : Number) : String
Expand All @@ -100,6 +100,14 @@ class Clear::Expression
{"(", x.to_sql, ")"}
end

def self.safe_literal(x : ::Clear::Expression::Node)
x.resolve
end

def self.safe_literal(x : Array(AvailableLiteral)) : Array(String)
x.map { |item| self.safe_literal(item) }
end

#
# Safe literal of a time is the time in the database format
# @params date
Expand Down Expand Up @@ -190,8 +198,8 @@ class Clear::Expression

macro method_missing(call)
{% if call.args.size > 0 %}
args = {{call.args}}.map{|x| Clear::Expression[x] }.join(", ")
return Node::Variable.new("{{call.name.id}}( #{args} )")
args = {{call.args}}.map{ |x| Clear::Expression[x] }.join(", ")
return Node::Variable.new("{{call.name.id}}(#{args})")
{% else %}
return Node::Variable.new({{call.name.id.stringify}})
{% end %}
Expand Down
14 changes: 14 additions & 0 deletions src/clear/expression/nodes/pg_array.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require "./node"

# PG structure `array[args...]`
# Named PGArray instead of Array to avoid issue with naming
class Clear::Expression::Node::PGArray(T) < Clear::Expression::Node
@arr : Array(T)

def initialize(@arr : Array(T))
end

def resolve
{"array[", Clear::Expression[@arr].join(", "), "]"}.join
end
end
4 changes: 2 additions & 2 deletions src/clear/expression/nodes/variable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class Clear::Expression::Node::Variable < Clear::Expression::Node

macro method_missing(call)
{% if call.args.size > 0 %}
args = {{call.args}}.map{|x| Clear::Expression[x] }.join(", ")
return Node::Variable.new("{{call.name.id}}( #{args} )", self)
args = Clear::Expression[{{call.args}}].join(", ")
return Node::Variable.new("{{call.name.id}}(#{args})", self)
{% else %}
return Node::Variable.new({{call.name.id.stringify}}, self)
{% end %}
Expand Down
40 changes: 18 additions & 22 deletions src/clear/sql/jsonb/expression.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ class Clear::Expression::Node::JSONB::Field < Clear::Expression::Node

getter field : Node
getter key : String
getter cast : String?

def initialize(@field, @key)
def initialize(@field, @key, @cast = nil)
end

def resolve
jsonb_resolve(@field.resolve, jsonb_k2a(key))
jsonb_resolve(@field.resolve, jsonb_k2a(key), @cast)
end

def cast(@cast)
self
end

def ==(value : Clear::Expression::Node)
Expand All @@ -35,35 +40,26 @@ class Clear::Expression::Node::JSONB::Equality < Clear::Expression::Node
def resolve
{@jsonb_field, Clear::Expression[@value.to_json]}.join(" @> ")
end

# In case of AND with another JSON equality test
# we merge both expression in only one !
def &(other : self)
if (other.jsonb_field == jsonb_field)
Clear::Expression::Node::JSONB::Equality.new(jsonb_field,
Clear::Util.hash_union(value, other.value)
)
else
super(other)
end
end
end

class Clear::Expression::Node::Variable < Clear::Expression::Node
class Clear::Expression::Node
def jsonb_key_exists?(key : String)
Clear::Expression::Node::DoubleOperator.new(self, Clear::Expression::Node::Literal.new(key), "?")
end

def jsonb_any_key_exists?(keys : Array(String))
# :no_doc:
private def _jsonb_keys_exists(keys : Array(T), op) forall T
Clear::Expression::Node::DoubleOperator.new(self,
{"array[", keys.join(", "), "]"}.join
,"?|")
Clear::Expression::Node::PGArray(T).new(keys)
,op)
end

def jsonb_all_keys_exists?(keys : Array(String))
Clear::Expression::Node::DoubleOperator.new(self,
{"array[", keys.join(", "), "]"}.join
,"?&")
def jsonb_any_key_exists?(keys : Array(T)) forall T
_jsonb_keys_exists(keys, "?|")
end

def jsonb_all_keys_exists?(keys : Array(T)) forall T
_jsonb_keys_exists(keys, "?&")
end

def jsonb(key : String)
Expand Down
17 changes: 6 additions & 11 deletions src/clear/sql/jsonb/jsonb.cr
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,11 @@ module Clear::SQL::JSONB
end
end

def jsonb_resolve(field, arr : Array(String)) : String
def jsonb_resolve(field, arr : Array(String), cast = nil) : String
return field if arr.empty?

arr = arr.map { |x| Clear::Expression[x] }

if arr.size == 1
{field, arr[0]}.join("->>")
else
{([field] + arr[0..-2]).join("->"), arr[-1]}.join("->>")
end
o = ([field] + Clear::Expression[arr]).join("->")
o += "::#{cast}" if cast
o
end

# Return text selector for the field/key :
Expand All @@ -139,8 +134,8 @@ module Clear::SQL::JSONB
# # => "data->'sub'->>'key' LIKE 'user%'"
# ```
#
def jsonb_resolve(field, key : String)
def jsonb_resolve(field, key : String, cast = nil)
arr = jsonb_k2a(key)
jsonb_resolve(field, arr)
jsonb_resolve(field, arr, cast)
end
end

0 comments on commit bf9740a

Please sign in to comment.