Skip to content

Commit 226b92c

Browse files
committed
overhaul
Meet new Core rewritten from scratch! Featuring new schema declaration syntax, type-safe querying, spec splitting and overall code reduction! Removed functionality: - Validations have been removed. Use external shards instead if needed. Closes #46 - Repository `#insert`, `#update`, `#delete` and all query other than `#query` methods are removed in favour of type safety, thus closing #47 and closing #61 and also closing #33 - Converters concept has been liquidated, types now rely on `#to_db` and `#from_rs` methods via monkey patching, which closes #60 New schema declaration syntax closes #58 and closes #52. Rewritten Query resolves #48. The overhaul itself closes #55 as well Hope you guys enjoy it!
1 parent 57a6c80 commit 226b92c

File tree

120 files changed

+3631
-4072
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+3631
-4072
lines changed

.editorconfig

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
root = true
2+
13
[*.cr]
24
charset = utf-8
35
end_of_line = lf

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
/doc/
1+
/docs/
22
/lib/
33
/bin/
44
/.shards/
5+
*.dwarf
56

67
# Libraries don't need dependency lock
78
# Dependencies will be locked in application that uses them

.travis.yml

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
language: crystal
2+
crystal: nightly
23
services:
34
- postgresql
45
script:
56
- crystal spec
7+
- env POSTGRESQL_URL=$POSTGRESQL_URL crystal spec db_spec
68
- crystal docs
79
deploy:
810
provider: pages
@@ -12,7 +14,7 @@ deploy:
1214
branch: master
1315
local_dir: docs
1416
before_script:
15-
- psql -c 'create database core_test;' -U postgres
16-
- psql $DATABASE_URL < spec/migration.sql
17+
- psql -c 'create database test;' -U postgres
18+
- psql $POSTGRESQL_URL < spec/migration.sql
1719
env:
18-
- DATABASE_URL=postgres://postgres@localhost:5432/core_test
20+
- POSTGRESQL_URL=postgres://postgres@localhost:5432/test

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2017 Vlad Faust
3+
Copyright (c) 2017-2018 Vlad Faust
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

+67-55
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,36 @@
1-
# ![Core](https://user-images.githubusercontent.com/7955682/40578252-6f1929b2-6119-11e8-9348-81505cec939f.png)
1+
> ⚠️ Master branch requires Crystal master to compile. See [installation instructions for Crystal](https://crystal-lang.org/docs/installation/from_source_repository.html).
2+
3+
![Core](https://user-images.githubusercontent.com/7955682/40578252-6f1929b2-6119-11e8-9348-81505cec939f.png)
4+
5+
Type-safe and expressive SQL ORM for [Crystal](https://crystal-lang.org).
26

37
[![Built with Crystal](https://img.shields.io/badge/built%20with-crystal-000000.svg?style=flat-square)](https://crystal-lang.org/)
48
[![Build status](https://img.shields.io/travis/vladfaust/core/master.svg?style=flat-square)](https://travis-ci.org/vladfaust/core)
59
[![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg?style=flat-square)](https://github.vladfaust.com/core)
610
[![Releases](https://img.shields.io/github/release/vladfaust/core.svg?style=flat-square)](https://github.com/vladfaust/core/releases)
7-
[![Awesome](https://img.shields.io/badge/style-awesome-lightgrey.svg?longCache=true&style=flat-square&label=&colorA=fc60a8&colorB=494368&status=ok&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgd2lkdGg9IjE1NC43ODEyNW1tIiAgIGhlaWdodD0iODAuMTE1ODI5bW0iICAgdmlld0JveD0iMCAwIDE1NC43ODEyNSA4MC4xMTU4MjkiICAgdmVyc2lvbj0iMS4xIiAgIGlkPSJzdmc4IiAgIGlua3NjYXBlOnZlcnNpb249IjAuOTIuMSByMTUzNzEiICAgc29kaXBvZGk6ZG9jbmFtZT0iYXdlc29tZS5zdmciPiAgPGRlZnMgICAgIGlkPSJkZWZzMiIgLz4gIDxzb2RpcG9kaTpuYW1lZHZpZXcgICAgIGlkPSJiYXNlIiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiICAgICBib3JkZXJvcGFjaXR5PSIxLjAiICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIgICAgIGlua3NjYXBlOnpvb209IjAuNyIgICAgIGlua3NjYXBlOmN4PSIxMzMuMTU2NTYiICAgICBpbmtzY2FwZTpjeT0iMTAxLjUzNjMiICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ibW0iICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiICAgICBzaG93Z3JpZD0iZmFsc2UiICAgICBmaXQtbWFyZ2luLXRvcD0iMCIgICAgIGZpdC1tYXJnaW4tbGVmdD0iMCIgICAgIGZpdC1tYXJnaW4tcmlnaHQ9IjAiICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIgICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjEwMTciICAgICBpbmtzY2FwZTp3aW5kb3cteD0iLTgiICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTgiICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIiAvPiAgPG1ldGFkYXRhICAgICBpZD0ibWV0YWRhdGE1Ij4gICAgPHJkZjpSREY+ICAgICAgPGNjOldvcmsgICAgICAgICByZGY6YWJvdXQ9IiI+ICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4gICAgICAgIDxkYzp0eXBlICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPiAgICAgICAgPGRjOnRpdGxlPjwvZGM6dGl0bGU+ICAgICAgPC9jYzpXb3JrPiAgICA8L3JkZjpSREY+ICA8L21ldGFkYXRhPiAgPGcgICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIgICAgIGlkPSJsYXllcjEiICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMzIuMjA5MjQzLC05OS4zODI3MDcpIj4gICAgPHBhdGggICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtzdHJva2Utd2lkdGg6MC4yNjQ1ODMzMiIgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgICAgICAgZD0ibSAxODYuOTkwNDksMTM1LjgxNTgzIC0zOS42ODc1LC0zNi40MDY2NjQgLTUuNTgyNzEsNi4wODU0MTQgMzMuMDcyOTIsMzAuMzIxMjUgSCA0NC40MzI5OTQgTCA3Ny41MDU5MSwxMDUuNDY4MTIgNzEuOTIzMjAyLDk5LjM4MjcwNyAzMi4yMzU3MDMsMTM1LjgxNTgzIGggLTAuMDI2NDYgdiAyMy45OTc3MSBjIDAsMTAuODQ3OTEgMTAuNDUxMDQxLDE5LjY4NSAyMy4yODMzMzIsMTkuNjg1IGggMjQuNDczOTU4IGMgMTIuODMyMjkyLDAgMjMuMjgzMzM3LC04LjgzNzA5IDIzLjI4MzMzNywtMTkuNjg1IHYgLTE1Ljc2OTE3IGggMTIuNyB2IDE1Ljc2OTE3IGMgMCwxMC44NDc5MSAxMC40NTEwNCwxOS42ODUgMjMuMjgzMzMsMTkuNjg1IGggMjQuNDczOTYgYyAxMi44MzIyOSwwIDIzLjI4MzMzLC04LjgzNzA5IDIzLjI4MzMzLC0xOS42ODUgeiIgICAgICAgaWQ9InBhdGg0NDg3IiAvPiAgPC9nPjwvc3ZnPg==)](https://github.com/veelenga/awesome-crystal)
11+
[![Gitter Chat](https://img.shields.io/badge/style-chat-ed1965.svg?longCache=true&style=flat-square&label=&logo=gitter-white&colorA=555)](https://gitter.im/core-orm/Lobby)
12+
[![Awesome](https://github.com/vladfaust/awesome/blob/badge-flat-alternative/media/badge-flat-alternative.svg)](https://github.com/veelenga/awesome-crystal)
813
[![vladfaust.com](https://img.shields.io/badge/style-.com-lightgrey.svg?longCache=true&style=flat-square&label=vladfaust&colorB=0a83d8)](https://vladfaust.com)
914

10-
Core is an expressive modular ORM for [Crystal](https://crystal-lang.org) featuring:
11-
12-
- ⚡️ **Efficiency** based on [Crystal](https://crystal-lang.org) performance
13-
-**Expressiveness** with powerful DSL and lesser code
14-
- 💼 **Safety** with strictly typed attributes
15-
1615
## About
1716

18-
Core does not follow Active Record pattern, it's more like a data-mapping solution. There is a concept of Repository, which is basically a gateway to the database. For example:
17+
Core is a [crystal-db](https://github.com/crystal-lang/crystal-db) ORM which does not follow Active Record pattern, it's more like a data-mapping solution. There is a concept of Repository, which is basically a gateway to the database. For example:
1918

2019
```crystal
2120
repo = Core::Repository.new(db)
22-
users = repo.query(User, "SELECT * FROM users WHERE id > 42")
23-
users.class # => Array(User)
21+
users = repo.query(User.where(id: 42)).first
22+
users.class # => User
2423
```
2524

2625
Core also has a plently of features, including:
2726

28-
- Expressive Query builder, either standalone or module, allowing to use constructions like `Post.join(:author).where(author: user)`, which turns into a plain SQL
29-
- References preloader (the example above would return a `Post` which has `#author = <User @id=42>` attribute)
30-
- Validations module allowing to perform both inline and custom validations (`user.valid? # => true`)
27+
- Expressive and **type-safe** Query builder, allowing to use constructions like `Post.join(:author).where(author: user)`, which turns into a plain SQL
28+
- References preloader (the example above would return a `Post` which has `#author = <User @id=42>` attribute set)
29+
- Beautiful schema definition syntax
30+
31+
However, Core is designed to be minimal, so it doesn't perform tasks you may got used to, for example, it doesn't do database migrations itself. You may use [migrate](https://github.com/vladfaust/migrate.cr) instead. Also its Query builder is not intended to fully replace SQL but instead to help a developer to write less and safer code.
3132

32-
However, Core is designed to be minimal, so it doesn't perform task you may got used to, for example, it doesn't do database migrations itself. You may use [migrate](https://github.com/vladfaust/migrate.cr) instead.
33+
Also note that although Core code is designed to be abstract sutiable for any [crystal-db](https://github.com/crystal-lang/crystal-db) driver, it currently works with PostgreSQL only. But it's fairly easy to implement other drivers like MySQL or SQLite (see `/src/core/ext/pg` and `/src/core/repository.cr`).
3334

3435
## Installation
3536

@@ -39,88 +40,99 @@ Add this to your application's `shard.yml`:
3940
dependencies:
4041
core:
4142
github: vladfaust/core
42-
version: ~> 0.4.2
43+
version: ~> 0.5.0
4344
```
4445
4546
This shard follows [Semantic Versioning v2.0.0](http://semver.org/), so check [releases](https://github.com/vladfaust/core/releases) and change the `version` accordingly.
4647

4748
## Basic example
4849

49-
Assuming following database schema:
50+
Assuming following database migration:
5051

5152
```sql
5253
CREATE TABLE users(
53-
id SERIAL PRIMARY KEY,
54-
name VARCHAR(100) NOT NULL,
55-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
54+
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
55+
name VARCHAR(100) NOT NULL,
56+
age INT,
57+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
5658
);
5759
5860
CREATE TABLE posts(
59-
id SERIAL PRIMARY KEY,
60-
author_id INT NOT NULL REFERENCES users (id),
61-
content TEXT NOT NULL,
62-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
63-
updated_at TIMESTAMPTZ
61+
id SERIAL PRIMARY KEY,
62+
author_uuid INT NOT NULL REFERENCES users (uuid),
63+
content TEXT NOT NULL,
64+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
65+
updated_at TIMESTAMPTZ
6466
);
6567
```
6668

69+
Crystal code:
70+
6771
```crystal
72+
require "pg"
6873
require "core"
69-
require "pg" # Or maybe another driver
7074
7175
class User
7276
include Core::Schema
73-
include Core::Query
74-
include Core::Validations
7577
76-
schema :users do
77-
primary_key :id
78-
reference :posts, Array(Post), foreign_key: :author_id
78+
schema users do
79+
pkey uuid : UUID # UUIDs are supported out of the box
80+
81+
type name : String # Has NOT NULL in the column definition
82+
type age : Union(Int32 | Nil) # Does not have NULL in the column definition
83+
type created_at : Time = DB::Default # Has DEFAULT in the column definition
7984
80-
field :name, String, validate: {size: (3..100)}
81-
field :created_at, Time, db_default: true # Means that DB is handling the default value
85+
type posts : Array(Post), foreign_key: "author_uuid" # That is an implicit reference
8286
end
8387
end
8488
8589
class Post
8690
include Core::Schema
87-
include Core::Query
88-
include Core::Validations
8991
90-
schema :posts do
91-
primary_key :id
92-
reference :author, User, key: :author_id
92+
schema posts do
93+
pkey id : Int32
94+
95+
type author : User, key: "author_id" # That is an explicit reference
96+
type content : String
9397
94-
field :content, String
95-
field :created_at, Time, db_default: true
96-
field :updated_at, Time?
98+
type created_at : Time = DB::Default
99+
type updated_at : Union(Time | Nil)
97100
end
98101
end
99102
100-
db = DB.open(ENV["DATABASE_URL"])
101103
query_logger = Core::Logger::IO.new(STDOUT)
102-
repo = Core::Repository.new(db, query_logger)
104+
repo = Core::Repository.new(DB.open(ENV["DATABASE_URL"]), query_logger)
105+
106+
# Most of the query builder methods (e.g. insert) are type-safe
107+
user = repo.query(User.insert(name: "Vlad")).first
108+
post = repo.query(Post.insert(author: user, content: "What a beauteful day!")).first # Oops
109+
110+
# Logging to STDOUT:
111+
# [postgresql] INSERT INTO posts (author_uuid, content) VALUES (?, ?) RETURNING *
112+
# 1.708ms
113+
# [map] Post
114+
# 126μs
103115
104-
user = User.new(name: "Vl")
105-
user.valid? # => false
106-
user.errors # => [{:name => "must have size in range of 3..100"}]
107-
user.name = "Vlad"
108-
user = repo.insert(user)
116+
# #to_s returns raw SQL string, and for superiour performance you can store them in constants
117+
QUERY = Post.update.set(content: "placeholder").where(id: 0).to_s
118+
# UPDATE posts SET content = ? WHERE (id = ?)
109119
110-
post = repo.insert(Post.new(author: user, content: "What a beauteful day!")) # Oops
120+
# Would not return anything, however, doesn't check for incoming params types
121+
repo.exec(QUERY, "What a beautiful day!", post.id)
111122
112-
post.content = "What a beautiful day!"
113-
repo.update(post)
123+
# Join with preloading references
124+
posts = repo.query(Post.select('*').where(author: user).join(:author, select: {'*'}))
114125
115-
posts = repo.query(Post.where(author: user).join(:author))
116126
puts posts.first.inspect
117-
# => <Post @author=<User @name="Vlad"> @content="What a beautiful day!">
127+
# => <Post @id=42 @author=<User @name="Vlad" @uuid="..."> @content="What a beautiful day!">
118128
```
119129

120130
## Testing
121131

122-
1. Apply migration from `./spec/migration.sql`
123-
2. Run `env DATABASE_URL=your_database_url crystal spec`
132+
1. Run generic specs with `crystal spec`
133+
2. Apply migrations from `./db_spec/*/migration.sql`
134+
3. Run DB-specific specs with `env POSTGRESQL_URL=postgres://postgres:postgres@localhost:5432/core crystal spec db_spec`
135+
4. Optionally run benchmarks with `crystal bench.cr --release`
124136

125137
## Contributing
126138

bench.cr

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require "./bench/**"

bench/bench_helper.cr

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
require "benchmark"
2+
require "colorize"
3+
require "../spec/models"
4+
5+
COLORS = {
6+
header: :yellow,
7+
subheader: :blue,
8+
success: :green,
9+
}

bench/logger_bench.cr

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
require "./bench_helper"
2+
3+
puts "\nRunning Core::Logger benchmarks...\n".colorize(COLORS["header"])
4+
5+
def devnull
6+
File.open(File::DEVNULL, mode: "w")
7+
end
8+
9+
logger = Logger.new(devnull, Logger::DEBUG)
10+
11+
elapsed = Time.measure do
12+
Benchmark.ips do |x|
13+
io = Core::Logger::IO.new(devnull)
14+
15+
x.report "io w/ colors" do
16+
io.wrap("foo") { nil }
17+
end
18+
19+
io = Core::Logger::IO.new(devnull, false)
20+
21+
x.report "io w/o colors" do
22+
io.wrap("foo") { nil }
23+
end
24+
25+
std_logger = Core::Logger::Standard.new(logger, Logger::Severity::INFO)
26+
27+
x.report "logger w/ colors" do
28+
std_logger.wrap("foo") { nil }
29+
end
30+
31+
std_logger = Core::Logger::Standard.new(logger, Logger::Severity::INFO, false)
32+
33+
x.report "logger w/o colors" do
34+
std_logger.wrap("foo") { nil }
35+
end
36+
end
37+
end
38+
39+
puts "\nCompleted in #{TimeFormat.auto(elapsed)} ✔️".colorize(COLORS["success"])

0 commit comments

Comments
 (0)