Skip to content

Commit

Permalink
v0.6
Browse files Browse the repository at this point in the history
- Release of 0.6; See CHANGELOG.md for all the changes
  • Loading branch information
anykeyh authored Feb 25, 2019
1 parent 4fae4a6 commit d201f99
Show file tree
Hide file tree
Showing 82 changed files with 1,314 additions and 408 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
language: crystal
script:
- crystal spec
- crystal spec -Dquiet
- bin/ameba
- crystal docs
services:
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CRYSTAL_BIN ?= $(shell which crystal)

test:
$(CRYSTAL_BIN) spec -Dquiet
6 changes: 0 additions & 6 deletions bin/install_cli.sh

This file was deleted.

52 changes: 48 additions & 4 deletions manual/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,56 @@
# master/HEAD (v0.6)
# v0.6

v0.6 should have shipped polymorphic relations, spec rework and improvement in
documentation. That's a lot of work (honestly the biggest improvement since v0)
and since already a lot of stuff have been integrated, I think it's better to
ship now and prepare it for the next release.

Since few weeks I'm using Clear in a full-time project, so I can see and correct
many bugs. Clear should now be more stable in term of compilation and should not
crash the compiler (which happened in some borderline cases).

## Features

- [EXPERIMENTAL] Add `<<` operation on collection which comes from `has_many` and `has_many through:`
- [EXPERIMENTAL] add `unlink` operation on collection which comes from `has_many through:`
- [EXPERIMENTAL] Add `unlink` method on collection which comes from `has_many through:`
- [EXPERIMENTAL] Add possibility to create model from JSON:

```crystal
json = JSON.parse(%({"first_name": "John", "last_name": "Doe", "tags": ["customer", "medical"] }))
User.new(json)
```

- Add of `pluck` and `pluck_col` methods to retrieve one or multiple column in a Tuple,
which are super super fast and convenient!
- Add `Clear.with_cli` method to allow to use the CLI in your project. Check the documentation !
- Release of a guide and documentation to use Clear: https://clear.gitbook.io/project/
- Comments of source code
- SelectQuery now inherits from `Enumerable(Hash(String, Clear::SQL::Any))`
- Additional comments in the source code
- `SelectQuery` now inherits from `Enumerable(Hash(String, Clear::SQL::Any))`
- Add optional block on `Enum` definition. This allow you to define custom methods for the enum:
```crystal
Clear.enum ClientType, "company", "non_profit", "personnal" do
def pay_vat?
self == Personnal
end
end
```
- Add `?` support in `raw` method:
```crystal
a = 1
b = 1000
c = 2
where{ raw("generate_series(?, ?, ?)", a, b, c) }
```

## Breaking changes
- Migration: use of `table.column` instead of `table.${type}` (remove the method missing method); this was causing headache
in some case where the syntax wasn't exactly followed, as the error output from the compiler was unclear.
- Renaming of `with_serial_pkey` to `primary_key`; refactoring of the macro-code allowing to add other type of keys.
- Now allow `text`, `int` and `bigint` primary key, with the 0.5 `uid`, `serial` and `bigserial` primary keys.
- Renaming of `Clear::Model::InvalidModelError` to `Clear::Model::InvalidError` and `Clear::Model::ReadOnlyError` to
`Clear::Model::ReadOnly` to simplify as those classes are already in the `Clear::Model` namespace
- `Model#set` methods has been transfered to `Model#reset`, and `Model#set` now change the status of the column to
dirty. (see #81)

## Bug fixes
- Fix #66, #62
Expand Down
2 changes: 1 addition & 1 deletion manual/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* [Triggers](model/lifecycle/callbacks.md)
* [Batchs operations](model/batchs-operations/README.md)
* [Bulk update](model/batchs-operations/bulk-update.md)
* [Bulk insert](model/batchs-operations/bulk-insert.md)
* [Bulk insert & delete](model/batchs-operations/bulk-insert.md)
* [Transactions & Save Points](model/transactions-and-save-points/README.md)
* [Transaction & Savepoints](model/transactions-and-save-points/transaction.md)
* [Connection pool](model/transactions-and-save-points/connection-pool.md)
Expand Down
1 change: 0 additions & 1 deletion manual/migrations/migration-cli.md
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
# Migration CLI

22 changes: 21 additions & 1 deletion manual/model/batchs-operations/bulk-update.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,22 @@
# Bulk update
## Bulk update

Any simple query can be transformed to `update` query. The new update query will use the `where` clause as parameter
for the update.

```ruby
User.query.where(name =~ /[A-Z]/ ).
to_update.set(name: Clear::SQL.unsafe("LOWERCASE(name)")).execute
```

## Bulk delete

Same apply for DELETE query.

```ruby
User.query.where(name !~ /[A-Z]/ ).
to_delete.execute
```

{% hint style="warning" %}
Beware: Bulk update and delete do not trigger any model lifecycle hook. Proceed with care.
{% endhint %}
78 changes: 78 additions & 0 deletions manual/model/column-types/converters.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,80 @@
# Converters

Any type from PostgreSQL can be converted using converter objects.
By default, Clear converts already the main type of PostgreSQL.

However, custom type may not be supported yet. Clear offers you the possibility to add a custom converter.

## Declare a new converter

The example below with a converter for a `Color` structure shoudl be straight-forward:

```ruby
require "./base"

struct MyApp::Color
property r : UInt8 = 0
property g : UInt8 = 0
property b : UInt8 = 0
property a : UInt8 = 0

def to_s
# ...
end

def self.from_string(x : String)
# ...
end

def self.from_slice(x : Slice(UInt8))
# ...
end
end

class MyApp::ColorConverter
def self.to_column(x) : MyApp::Color?
case x
when Nil
nil
when Slice(UInt8)
MyApp::Color.from_slice(x)
when String
MyApp::Color.from_string(x)
else
raise "Cannot convert from #{x.class} to MyApp::Color"
end
end

def self.to_db(x : MyApp::Color?)
x.to_s #< css style output, e.g. #12345400
end
end

Clear::Model::Converter.add_converter("MyApp::Color", MyApp::ColorConverter)
```

Then you can use your mapped type in your model:

```ruby
class MyApp::MyModel
include Clear::Model
#...
column color : Color #< Automatically get the converter
end
```

## `converter` option

Optionally, you may want to use a converter which is not related to the type itself. To do so, you can pass the
converter name as optional argument in the `column` declaration:

```ruby
class MyApp::MyModel
include Clear::Model
#...
column s : String, converter: "my_custom_converter"
end
```

By convention, converters which map struct and class directly are named using CamelCase, while converters which are not
automatic should be named using the underscore notation.
12 changes: 6 additions & 6 deletions manual/model/column-types/primary-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ Clear needs your model to define a primary key column. By default, Clear can han
As time of writing this manual, compound primary keys are not handled properly.
{% endhint %}

## `with_serial_pkey` helper
## `primary_key` helper

Clear offers a built-in `with_serial_pkey` helper which will define your primary key without hassle:
Clear offers a built-in `primary_key` helper which will define your primary key without hassle:

```ruby
class Product
include Clear::Model

self.table = "products"

with_serial_pkey name: "product_id", type: :uuid
primary_key name: "product_id", type: :uuid
end
```

* `name` is the name of your column in your table. Set to `id` by default
* `type` is the type of the column in your table. Set to `bigserial` by default.
* type can be of type `bigserial`, `serial`, `text` and `uuid`.
* `name` is the name of your column in your table. (Default: `id`)
* `type` is the type of the column in your table. Set to (Default: `bigserial`).
* By default, types can be of type `bigserial`, `serial`, `int`, `bigint`, `text` and `uuid`.

{% hint style="info" %}
In case of `uuid`, Clear will generate a new `uuid` at every new object creation before inserting it into the database.
Expand Down
7 changes: 3 additions & 4 deletions manual/model/lifecycle/validations.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,12 @@ class Article
column description : String

def validate
ensure_than :description, "must contains at least 100 characters",
&.size.<(100)
ensure_than :description, "must contains at least 100 characters", &.size.<(100)
end
end
```

The code above will perform exactly like the previous one, with focus on compact syntax.
The code above will perform exactly like the previous one, while keeping a more compact syntax.

## Error object

Expand All @@ -67,7 +66,7 @@ a.content = "Lorem ipsum"

unless a.valid?
a.errors.each do |err|
puts "Error on column: #{err.column} => #{err.reason}"
puts "Error on column: #{err.column} => #{err.reason}"
end
end
```
Expand Down
2 changes: 1 addition & 1 deletion manual/model/transactions-and-save-points/transaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Clear::SQL.transaction do
Clear::SQL.rollback
puts "This should not print"
end
puts "Eventually, I do something else"
puts "This will never reach too."
end
```

Expand Down
19 changes: 10 additions & 9 deletions manual/other-resources/benchmark.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ Another good performance improvement would be to connect through [PGBouncer](htt

## Query and fetching benchmark

Here is a simple benchmark comparing the different layers of Clear and how they impact the performance, over a 100k row simple table:

| Method | Total Time | Speed |
| :--- | :--- | :--- |
| `User.query.each` | \( 83.03ms\) \(± 3.87%\) | 2.28× slower |
| `User.query.each_with_cursor` | \( 121.0ms\) \(± 1.25%\) | 3.32× slower |
| `User.query.each(fetch_columns: true)` | \( 97.12ms\) \(± 4.07%\) | 2.67× slower |
| `User.query.each_with_cursor(fetch_columns: true)` | \(132.52ms\) \(± 2.39%\) | 3.64× slower |
| `User.query.fetch` | \( 36.42ms\) \(± 5.05%\) | fastest |
Here is a simple benchmark comparing the different layers of Clear and how they impact the performance, over a 100k row very simple table:

```
With Model: With attributes and cursor 7.4 (135.09ms) (± 6.44%) 116409530 B/op 5.64× slower
With Model: With cursor 8.61 (116.08ms) (± 2.82%) 97209247 B/op 4.84× slower
With Model: With attributes 13.78 ( 72.59ms) (± 3.61%) 83101520 B/op 3.03× slower
With Model: Simple load 100k 16.41 ( 60.94ms) (± 3.22%) 63901872 B/op 2.54× slower
Hash from SQL only 30.21 ( 33.1ms) (± 5.18%) 22354496 B/op 1.38× slower
Using: Model::Collection#pluck 41.74 ( 23.96ms) (± 8.35%) 25337128 B/op fastest
```

## Against the competition

Expand Down
2 changes: 1 addition & 1 deletion manual/querying/the-collection-object/joins.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Joins are built using `inner_join`, `left_join`, `right_join`, `cross_join` or s

```ruby
# Retrieve users with supervisors
User.query.left_joins("users as u2"){ users.supervisor_id = u2.id }
User.query.left_joins("users as u2"){ users.supervisor_id == u2.id }
```

Additionally, optional parameter `lateral` can be set to true to create a LATERAL JOIN.
Expand Down
18 changes: 9 additions & 9 deletions manual/querying/the-collection-object/window-and-cte.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@ In this case, using joins onto a generated series of day is the way to go. CTE m

```ruby
dates_in_september = Clear::SQL.select({
day_start: "generate_series(date '2018-09-01', date '2018-09-30', '1 day'::interval)",
day_start: "generate_series(date '2018-09-01', date '2018-09-30', '1 day'::interval)",
day_end: "generate_series(date '2018-09-01', date '2018-09-30', '1 day'::interval) + '1 day'::interval";
})

Clear::SQL.select({
count: "COUNT(users.*)",
day: "dates.day_start"
})
.with_cte(dates: dates_in_septembers)
.from("dates")
.left_joins(User.table){ (users.created_at >= day_start) & (users.created_at < day_end) }
.group_by("dates.day_start")
.order_by("dates.day_start")
.fetch do |hash|
puts "users created the #{hash["day"]}: #{hash["count"]}"
end
.with_cte(dates: dates_in_septembers)
.from("dates")
.left_joins(User.table){ (users.created_at >= day_start) & (users.created_at < day_end) }
.group_by("dates.day_start")
.order_by("dates.day_start")
.fetch do |hash|
puts "users created the #{hash["day"]}: #{hash["count"]}"
end
```

{% hint style="info" %}
Expand Down
11 changes: 6 additions & 5 deletions sample/benchmark/model.cr
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ end
puts "Starting benchmarking, total to fetch =" +
" #{BenchmarkModel.query.count} records"
Benchmark.ips(warmup: 2, calculation: 5) do |x|
x.report("Simple load 100k") { BenchmarkModel.query.limit(100_000).to_a }
x.report("With cursor") { a = [] of BenchmarkModel; BenchmarkModel.query.limit(100_000).each_with_cursor { |o| a << o } }
x.report("With attributes") { BenchmarkModel.query.limit(100_000).to_a(fetch_columns: true) }
x.report("With attributes and cursor") { a = [] of BenchmarkModel; BenchmarkModel.query.limit(100_000).each_with_cursor(fetch_columns: true) { |h| a << h } }
x.report("SQL only") { a = [] of Hash(String, ::Clear::SQL::Any); BenchmarkModel.query.limit(100_000).fetch { |h| a << h } }
x.report("With Model: Simple load 100k") { BenchmarkModel.query.limit(100_000).to_a }
x.report("With Model: With cursor") { a = [] of BenchmarkModel; BenchmarkModel.query.limit(100_000).each_with_cursor { |o| a << o } }
x.report("With Model: With attributes") { BenchmarkModel.query.limit(100_000).to_a(fetch_columns: true) }
x.report("With Model: With attributes and cursor") { a = [] of BenchmarkModel; BenchmarkModel.query.limit(100_000).each_with_cursor(fetch_columns: true) { |h| a << h } }
x.report("Using: Pluck") { BenchmarkModel.query.limit(100_000).pluck("y") }
x.report("Hash from SQL only") { a = [] of Hash(String, ::Clear::SQL::Any); BenchmarkModel.query.limit(100_000).fetch { |h| a << h } }
end
4 changes: 3 additions & 1 deletion sample/cli/cli.cr
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ class ApplyChange2
end
end

Clear::CLI.run
Clear.with_cli do
puts "Usage: crystal sample/cli/cli.cr -- clear [args]"
end
2 changes: 1 addition & 1 deletion shard.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ shards:

ameba:
github: veelenga/ameba
version: 0.9.0
commit: d2b36047ef910defda7fb9acb6816a6fa6b6e917

db:
github: crystal-lang/crystal-db
Expand Down
6 changes: 3 additions & 3 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: clear
version: 0.5
version: 0.6

authors:
- Yacine Petitprez <[email protected]>
Expand Down Expand Up @@ -27,8 +27,8 @@ development_dependencies:
# github: anykeyh/crystal-coverage
ameba:
github: veelenga/ameba
version: "~> 0.9"
branch: master

crystal: 0.27.0
crystal: 0.27.2

license: MIT
Loading

0 comments on commit d201f99

Please sign in to comment.