Skip to content

Commit

Permalink
V0.8 (#124)
Browse files Browse the repository at this point in the history
* Fix TimeInDay issues
* Fix issue when using DISTINCT with JOIN in models with custom SELECT
clause defined AFTER the joins.
* Fix mistake in spec and add specs
* Add changelog; update shard
* Add seed command in the CLI
* add `or_where` feature
* Fix FTS to remove ambiguous clauses
* Fix issue with nilable belongs_to which cannot be saved when set to nil
* Add RFC3339 support while converting string to time
* Fix caching with belongs_to
* Add colorize parameter to Clear::SQL::Logger module
* Migration: Add datatype conversion in add_column and alter_column methods
* Migration: Update migration add_column operation to allow contraints, nullable
and default value
* Update to latest version of pg gem
* Fix ambigous column name in with_xxx method for belongs_to relation
* Add possibility to have nulls first and nulls last in `order_by` method
* WIP on a SQL parser
* Add the possibility to convert from Array(JSON:Any)
* Fix misc typos
  • Loading branch information
anykeyh authored Feb 2, 2020
1 parent 06491e0 commit 5e233e5
Show file tree
Hide file tree
Showing 30 changed files with 617 additions and 86 deletions.
27 changes: 27 additions & 0 deletions manual/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
# v0.8

## Features

- Add `or_where` clause

This provide a way to chain where clause with `or` operator instead of `and`:

```crystal
query.where{ a == b }.or_where{ b == c } # WHERE (A = B) OR (b = C)
query.where{ a == b }.where{ c == d}.or_where{ a == nil } # WHERE ( A=B AND C=D ) OR A IS NULL
```

- Add `raw` method into `Clear::SQL` module.

This provide a fast way to create SQL fragment while escaping items, both with `?` and `:key` system:

```crystal
query = Mode.query.select( Clear::SQL.raw("CASE WHEN x=:x THEN 1 ELSE 0 END as check", x: "blabla") )
query = Mode.query.select( Clear::SQL.raw("CASE WHEN x=? THEN 1 ELSE 0 END as check", "blabla") )
```

## Bugfixes

- Migrate to crystal v0.29
- Fix issue with combinaison of `join`, `distinct` and `select`

# v0.7

## Features
Expand Down
4 changes: 4 additions & 0 deletions sample/cli/cli.cr
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class ApplyChange2
end
end

Clear.seed do
puts "This is a seed"
end

Clear.with_cli do
puts "Usage: crystal sample/cli/cli.cr -- clear [args]"
end
3 changes: 2 additions & 1 deletion shard.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name: clear
version: 0.7.2

version: 0.8

authors:
- Yacine Petitprez <[email protected]>
Expand Down
45 changes: 45 additions & 0 deletions spec/model/model_different_column_name_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require "../spec_helper"

module ModelDifferentColumnNameSpec
class Brand
include Clear::Model

primary_key
column name : String, column_name: "brand_name"
self.table = "brands"
end

class ModelDifferentColumnNameSpecMigration8273
include Clear::Migration

def change(dir)
create_table "brands" do |t|
t.column "brand_name", "string"

t.timestamps
end
end
end

def self.reinit
reinit_migration_manager
ModelDifferentColumnNameSpecMigration8273.new.apply(Clear::Migration::Direction::UP)
end

describe "Clear::Model" do
context "Column definition" do
it "can define properties in the model with a name different of the column name in PG" do
# Here the column "name" is linked to "brand_name" in postgreSQL
temporary do
reinit

Brand.create! name: "Nike"
Brand.query.first!.name.should eq "Nike"
Brand.query.where(brand_name: "Nike").count.should eq 1
end
end
end
end


end
28 changes: 28 additions & 0 deletions spec/model/model_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,15 @@ module ModelSpec
#reload the model now
u.reload.first_name.should eq "Malcom"
u.changed?.should be_false

u2 = User.create!({id: 2, first_name: "y"})

p = Post.create! user: u, title: "Reload testing post"

p.user.id.should eq(1)
p.user = u2 #Change the user, DO NOT SAVE.
p.reload # Reload the model now:
p.user.id.should eq(1) # Cache should be invalidated
end
end

Expand Down Expand Up @@ -697,6 +706,25 @@ module ModelSpec
user_with_a_post_minimum.with_posts.each { } # Should just execute
end
end

it "should wildcard with default model only if no select is made (before OR after)" do
temporary do
reinit
u = User.create!({first_name: "Join User"})
Post.create!({title: "A Post", user_id: u.id})

user_with_a_post_minimum = User.query.distinct
.join(:model_posts) { model_posts.user_id == model_users.id }
.select(:first_name, :last_name)

user_with_a_post_minimum.to_sql.should eq \
"SELECT DISTINCT \"first_name\", \"last_name\" FROM \"model_users\" INNER JOIN " +
"\"model_posts\" ON (\"model_posts\".\"user_id\" = \"model_users\".\"id\")"

user_with_a_post_minimum.with_posts.each { } # Should just execute
end
end

end

context "with pagination" do
Expand Down
27 changes: 27 additions & 0 deletions spec/sql/misc_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,33 @@ module SQLMiscSpec

describe "Clear::SQL" do
describe "miscalleanous" do

it "can escape for SQL-safe object" do
Clear::SQL.escape("order").should eq "\"order\""
Clear::SQL.escape("").should eq "\"\""
Clear::SQL.escape(:hello).should eq "\"hello\""

Clear::SQL.escape("some.weird.column name").should eq "\"some.weird.column name\""
Clear::SQL.escape("\"hello world\"").should eq "\"\"\"hello world\"\"\""
end

it "can sanitize for SQL-safe string" do
Clear::SQL.sanitize(1).should eq "1"
Clear::SQL.sanitize("").should eq "''"
Clear::SQL.sanitize(nil).should eq "NULL"
Clear::SQL.sanitize("l'hotel").should eq "'l''hotel'"
end

it "can create SQL fragment" do
Clear::SQL.raw("SELECT * FROM table WHERE x = ?", "hello").should eq(
%(SELECT * FROM table WHERE x = 'hello')
)

Clear::SQL.raw("SELECT * FROM table WHERE x = :x", x: 1).should eq(
%(SELECT * FROM table WHERE x = 1)
)
end

it "can truncate a table" do

begin
Expand Down
21 changes: 21 additions & 0 deletions spec/sql/parser_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require "../spec_helper"

module ParserSpec
extend self

describe "Clear::SQL" do
describe "Parser" do

it "parse correctly" do
Clear::SQL::Parser.parse(<<-SQL
SELECT * FROM "users" where (id > 100 and active is null);
SELECT 'string' as text;
-- This is a comment
SQL
) do |token|
pp token
end
end
end
end
end
35 changes: 35 additions & 0 deletions spec/sql/select_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ module SelectSpec
r.to_sql.should eq "SELECT *"
end

it "can select distinct" do
r = select_request.distinct.select("*")
r.to_sql.should eq "SELECT DISTINCT *"

r = select_request.distinct.select("a", "b", "c")
r.to_sql.should eq "SELECT DISTINCT a, b, c"

r = select_request.distinct.select(:first_name, :last_name, :id)
r.to_sql.should eq "SELECT DISTINCT \"first_name\", \"last_name\", \"id\""
end

it "can select any string" do
r = select_request.select("1")
r.to_sql.should eq "SELECT 1"
Expand Down Expand Up @@ -80,6 +91,13 @@ module SelectSpec
end
end

describe "the ORDER BY clause" do
it "can add NULLS FIRST and NULLS LAST" do
r = select_request.from("users").order_by("email", "ASC", "NULLS LAST")
r.to_sql.should eq "SELECT * FROM users ORDER BY email ASC NULLS LAST"
end
end

describe "the FROM clause" do
it "can build simple from" do
r = select_request.from(:users)
Expand Down Expand Up @@ -156,6 +174,18 @@ module SelectSpec
r.to_sql.should eq "SELECT * FROM \"users\" WHERE (\"user_id\" IS NULL)"
end

it "can use or_where" do
select_request.from(:users).where("a = ?", {1}).or_where("b = ?", {2}).to_sql.should(
eq %(SELECT * FROM "users" WHERE ((a = 1) OR (b = 2)))
)

# First OR WHERE acts as a simple WHERE:
select_request.from(:users).or_where("a = ?", {1}).or_where("b = ?", {2}).to_sql.should(
eq %(SELECT * FROM "users" WHERE ((a = 1) OR (b = 2)))
)
end


it "can use `in` operators in case of array" do
r = select_request.from(:users).where({user_id: [1, 2, 3, 4, "hello"]})
r.to_sql.should eq "SELECT * FROM \"users\" WHERE \"user_id\" IN (1, 2, 3, 4, 'hello')"
Expand Down Expand Up @@ -199,6 +229,11 @@ module SelectSpec
select_request.from(:users).where("a LIKE :halo AND b LIKE :world",
{hello: "h", world: "w"})
end

expect_raises Clear::SQL::QueryBuildingError do
select_request.from(:users).or_where("a LIKE :halo AND b LIKE :world",
{hello: "h", world: "w"})
end
end

it "can prepare group by query" do
Expand Down
2 changes: 2 additions & 0 deletions src/clear/cli.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ require "admiral"
require "./core"
require "./cli/command"
require "./cli/migration"
require "./cli/seed"
require "./cli/generator"

module Clear
Expand All @@ -19,6 +20,7 @@ module Clear

register_sub_command migrate, type: Clear::CLI::Migration
register_sub_command generate, type: Clear::CLI::Generator
register_sub_command seed, type: Clear::CLI::Seed

def run_impl
STDOUT.puts help
Expand Down
23 changes: 0 additions & 23 deletions src/clear/cli/db.cr

This file was deleted.

10 changes: 10 additions & 0 deletions src/clear/cli/seed.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class Clear::CLI::Seed < Admiral::Command
include Clear::CLI::Command

define_help description: "Seed the database with seed data"

def run_impl
Clear.apply_seeds
end

end
62 changes: 60 additions & 2 deletions src/clear/expression/expression.cr
Original file line number Diff line number Diff line change
Expand Up @@ -235,17 +235,75 @@ class Clear::Expression
#
#
def raw(x : String, *args)
Node::Raw.new(self.class.raw(x, *args))
end


# In case the name of the variable is a reserved word (e.g. `not`, `var`, `raw` )
# or in case of a complex piece of computation impossible to express with the expression engine
# (e.g. usage of functions) you can use then raw to pass the String.
#
# BE AWARE than the String is pasted AS-IS and can lead to SQL injection if not used properly.
#
# ```
# having { raw("COUNT(*)") > 5 } # SELECT ... FROM ... HAVING COUNT(*) > 5
# where { raw("func(?, ?) = ?", a, b, c) } # SELECT ... FROM ... WHERE function(a, b) = c
# ```
#
#
def self.raw(x : String, *args)
raw_enum(x, args)
end

# See `self.raw`
# Can pass an array to this version
def self.raw_enum(x : String, args)
idx = -1

clause = x.gsub("?") do |_|
x.gsub("?") do |_|
begin
Clear::Expression[args[idx += 1]]
rescue e : IndexError
raise Clear::ErrorMessages.query_building_error(e.message)
end
end
end

# In case the name of the variable is a reserved word (e.g. `not`, `var`, `raw` )
# or in case of a complex piece of computation impossible to express with the expression engine
# (e.g. usage of functions) you can use then raw to pass the String.
#
# BE AWARE than the String is pasted AS-IS and can lead to SQL injection if not used properly.
#
# ```
# having { raw("COUNT(*)") > 5 } # SELECT ... FROM ... HAVING COUNT(*) > 5
# where { raw("func(:a, :b) = :c", a: a, b: b, c: c) } # SELECT ... FROM ... WHERE function(a, b) = c
# ```
#
def raw(__template : String, **tuple)
Node::Raw.new(self.class.raw(__template, **tuple))
end

Node::Raw.new(clause)
# In case the name of the variable is a reserved word (e.g. `not`, `var`, `raw` )
# or in case of a complex piece of computation impossible to express with the expression engine
# (e.g. usage of functions) you can use then raw to pass the String.
#
# BE AWARE than the String is pasted AS-IS and can lead to SQL injection if not used properly.
#
# ```
# having { raw("COUNT(*)") > 5 } # SELECT ... FROM ... HAVING COUNT(*) > 5
# where { raw("func(:a, :b) = :c", a: a, b: b, c: c) } # SELECT ... FROM ... WHERE function(a, b) = c
# ```
#
def self.raw(__template : String, **tuple)
__template.gsub(/\:[a-zA-Z0-9_]+/) do |question_mark|
begin
sym = question_mark[1..-1]
Clear::Expression[tuple[sym]]
rescue e : KeyError
raise Clear::ErrorMessages.query_building_error(e.message)
end
end
end

# Use var to create expression of variable. Variables are columns with or without the namespace and tablename:
Expand Down
Loading

0 comments on commit 5e233e5

Please sign in to comment.