Skip to content

Commit

Permalink
overhaul
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
vladfaust committed Sep 15, 2018
1 parent 57a6c80 commit 226b92c
Show file tree
Hide file tree
Showing 120 changed files with 3,631 additions and 4,072 deletions.
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
root = true

[*.cr]
charset = utf-8
end_of_line = lf
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/doc/
/docs/
/lib/
/bin/
/.shards/
*.dwarf

# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
Expand Down
8 changes: 5 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
language: crystal
crystal: nightly
services:
- postgresql
script:
- crystal spec
- env POSTGRESQL_URL=$POSTGRESQL_URL crystal spec db_spec
- crystal docs
deploy:
provider: pages
Expand All @@ -12,7 +14,7 @@ deploy:
branch: master
local_dir: docs
before_script:
- psql -c 'create database core_test;' -U postgres
- psql $DATABASE_URL < spec/migration.sql
- psql -c 'create database test;' -U postgres
- psql $POSTGRESQL_URL < spec/migration.sql
env:
- DATABASE_URL=postgres://postgres@localhost:5432/core_test
- POSTGRESQL_URL=postgres://postgres@localhost:5432/test
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2017 Vlad Faust
Copyright (c) 2017-2018 Vlad Faust

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
122 changes: 67 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
# ![Core](https://user-images.githubusercontent.com/7955682/40578252-6f1929b2-6119-11e8-9348-81505cec939f.png)
> ⚠️ Master branch requires Crystal master to compile. See [installation instructions for Crystal](https://crystal-lang.org/docs/installation/from_source_repository.html).
![Core](https://user-images.githubusercontent.com/7955682/40578252-6f1929b2-6119-11e8-9348-81505cec939f.png)

Type-safe and expressive SQL ORM for [Crystal](https://crystal-lang.org).

[![Built with Crystal](https://img.shields.io/badge/built%20with-crystal-000000.svg?style=flat-square)](https://crystal-lang.org/)
[![Build status](https://img.shields.io/travis/vladfaust/core/master.svg?style=flat-square)](https://travis-ci.org/vladfaust/core)
[![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg?style=flat-square)](https://github.vladfaust.com/core)
[![Releases](https://img.shields.io/github/release/vladfaust/core.svg?style=flat-square)](https://github.com/vladfaust/core/releases)
[![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)
[![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)
[![Awesome](https://github.com/vladfaust/awesome/blob/badge-flat-alternative/media/badge-flat-alternative.svg)](https://github.com/veelenga/awesome-crystal)
[![vladfaust.com](https://img.shields.io/badge/style-.com-lightgrey.svg?longCache=true&style=flat-square&label=vladfaust&colorB=0a83d8)](https://vladfaust.com)

Core is an expressive modular ORM for [Crystal](https://crystal-lang.org) featuring:

- ⚡️ **Efficiency** based on [Crystal](https://crystal-lang.org) performance
-**Expressiveness** with powerful DSL and lesser code
- 💼 **Safety** with strictly typed attributes

## About

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:
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:

```crystal
repo = Core::Repository.new(db)
users = repo.query(User, "SELECT * FROM users WHERE id > 42")
users.class # => Array(User)
users = repo.query(User.where(id: 42)).first
users.class # => User
```

Core also has a plently of features, including:

- Expressive Query builder, either standalone or module, allowing to use constructions like `Post.join(:author).where(author: user)`, which turns into a plain SQL
- References preloader (the example above would return a `Post` which has `#author = <User @id=42>` attribute)
- Validations module allowing to perform both inline and custom validations (`user.valid? # => true`)
- Expressive and **type-safe** Query builder, allowing to use constructions like `Post.join(:author).where(author: user)`, which turns into a plain SQL
- References preloader (the example above would return a `Post` which has `#author = <User @id=42>` attribute set)
- Beautiful schema definition syntax

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.

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.
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`).

## Installation

Expand All @@ -39,88 +40,99 @@ Add this to your application's `shard.yml`:
dependencies:
core:
github: vladfaust/core
version: ~> 0.4.2
version: ~> 0.5.0
```
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.

## Basic example

Assuming following database schema:
Assuming following database migration:

```sql
CREATE TABLE users(
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
age INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE posts(
id SERIAL PRIMARY KEY,
author_id INT NOT NULL REFERENCES users (id),
content TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
id SERIAL PRIMARY KEY,
author_uuid INT NOT NULL REFERENCES users (uuid),
content TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
```

Crystal code:

```crystal
require "pg"
require "core"
require "pg" # Or maybe another driver
class User
include Core::Schema
include Core::Query
include Core::Validations
schema :users do
primary_key :id
reference :posts, Array(Post), foreign_key: :author_id
schema users do
pkey uuid : UUID # UUIDs are supported out of the box
type name : String # Has NOT NULL in the column definition
type age : Union(Int32 | Nil) # Does not have NULL in the column definition
type created_at : Time = DB::Default # Has DEFAULT in the column definition
field :name, String, validate: {size: (3..100)}
field :created_at, Time, db_default: true # Means that DB is handling the default value
type posts : Array(Post), foreign_key: "author_uuid" # That is an implicit reference
end
end
class Post
include Core::Schema
include Core::Query
include Core::Validations
schema :posts do
primary_key :id
reference :author, User, key: :author_id
schema posts do
pkey id : Int32
type author : User, key: "author_id" # That is an explicit reference
type content : String
field :content, String
field :created_at, Time, db_default: true
field :updated_at, Time?
type created_at : Time = DB::Default
type updated_at : Union(Time | Nil)
end
end
db = DB.open(ENV["DATABASE_URL"])
query_logger = Core::Logger::IO.new(STDOUT)
repo = Core::Repository.new(db, query_logger)
repo = Core::Repository.new(DB.open(ENV["DATABASE_URL"]), query_logger)
# Most of the query builder methods (e.g. insert) are type-safe
user = repo.query(User.insert(name: "Vlad")).first
post = repo.query(Post.insert(author: user, content: "What a beauteful day!")).first # Oops
# Logging to STDOUT:
# [postgresql] INSERT INTO posts (author_uuid, content) VALUES (?, ?) RETURNING *
# 1.708ms
# [map] Post
# 126μs
user = User.new(name: "Vl")
user.valid? # => false
user.errors # => [{:name => "must have size in range of 3..100"}]
user.name = "Vlad"
user = repo.insert(user)
# #to_s returns raw SQL string, and for superiour performance you can store them in constants
QUERY = Post.update.set(content: "placeholder").where(id: 0).to_s
# UPDATE posts SET content = ? WHERE (id = ?)
post = repo.insert(Post.new(author: user, content: "What a beauteful day!")) # Oops
# Would not return anything, however, doesn't check for incoming params types
repo.exec(QUERY, "What a beautiful day!", post.id)
post.content = "What a beautiful day!"
repo.update(post)
# Join with preloading references
posts = repo.query(Post.select('*').where(author: user).join(:author, select: {'*'}))
posts = repo.query(Post.where(author: user).join(:author))
puts posts.first.inspect
# => <Post @author=<User @name="Vlad"> @content="What a beautiful day!">
# => <Post @id=42 @author=<User @name="Vlad" @uuid="..."> @content="What a beautiful day!">
```

## Testing

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

## Contributing

Expand Down
1 change: 1 addition & 0 deletions bench.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require "./bench/**"
9 changes: 9 additions & 0 deletions bench/bench_helper.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "benchmark"
require "colorize"
require "../spec/models"

COLORS = {
header: :yellow,
subheader: :blue,
success: :green,
}
39 changes: 39 additions & 0 deletions bench/logger_bench.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require "./bench_helper"

puts "\nRunning Core::Logger benchmarks...\n".colorize(COLORS["header"])

def devnull
File.open(File::DEVNULL, mode: "w")
end

logger = Logger.new(devnull, Logger::DEBUG)

elapsed = Time.measure do
Benchmark.ips do |x|
io = Core::Logger::IO.new(devnull)

x.report "io w/ colors" do
io.wrap("foo") { nil }
end

io = Core::Logger::IO.new(devnull, false)

x.report "io w/o colors" do
io.wrap("foo") { nil }
end

std_logger = Core::Logger::Standard.new(logger, Logger::Severity::INFO)

x.report "logger w/ colors" do
std_logger.wrap("foo") { nil }
end

std_logger = Core::Logger::Standard.new(logger, Logger::Severity::INFO, false)

x.report "logger w/o colors" do
std_logger.wrap("foo") { nil }
end
end
end

puts "\nCompleted in #{TimeFormat.auto(elapsed)} ✔️".colorize(COLORS["success"])
Loading

0 comments on commit 226b92c

Please sign in to comment.