pon.cr 
Maiha's private ORM for Crystal.
tested
- crystal: 1.6.0-alpine
- db: crystal-db-0.10.1
 - mysql: crystal-mysql-0.13.0
 - sqlite3: crystal-sqlite3-0.18.0
 - pg: will/crystal-pg-0.23.2
 
 - mysql:5.6
 - postgres:12.4
 
Usage
require "pon"
require "pon/adapter/mysql"  # "mysql", "pg", "sqlite"
# Enum support!
enum Code
  OK  = 200
  ERR = 500
end
# Model definition
class Job < Pon::Model
  adapter mysql              # "mysql", "pg", "sqlite"
  field   name : String
  field   time : Time
  field   code : Code = Code::OK  # default value
end
Pon::Adapter::Mysql.setting.url = "mysql://[email protected]:3306/test"
# Create table
Job.migrate! # drop and create the table
Job.count                 # => 0
# CRUD
job = Job.new(name: "foo")
job.name                  # => "foo"
job.time?                 # => nil
job.time                  # raises Pon::ValueNotFound
job.code                  # => Code::OK
job.save                  # => true
Job.find(job.id).code.ok? # => true
Job.create!(name: "bar", code: Code::ERR)
# Finder
Job.all.size                      # => 2
Job.all(where: "code = 200").size # => 1
Job.all(["code"]).map(&.name?)    # => [nil, nil]
# And more useful features
Job.pluck(["name"]) # => [["foo"], ["bar"]]
Job.count_by_code   # => {Code::OK => 1, Code::ERR => 1}
DataTypes
The following data types are supported (tested).
BoolEnum(Int32)Int32StringTime
See defined fields written in spec/spec_helper.cr.
API : Adapter
  def exec(sql) : Nil
  def lastval : Int64
  def scalar(*args)
  def reset! : Nil
  # CRUD
  def insert(fields, params)
  def all(fields : Array(String), types, condition = nil, **opts)
  def one?(id, fields : Array(String), as types : Tuple)
  def count : Int32
  def delete(key) : Bool
  def delete : Nil
  def truncate : Nil
  # ODBC
  def databases : Array(String)
  def tables : Array(String)
  # Experimental
  def transaction(&block) : Nil # only sqlite and pg
API : Model
class Pon::Model
  # Databases
  def self.adapter : Adapter(A)
  def self.migrator : Migrator
  def self.migrate! : Nil
  # Core
  def self.table_name : String
  def new_record? : Bool
  def to_h : Hash(String, ALL_TYPES)
  # CRUD
  def self.create! : M
  def self.create : M
  def self.count : Int32
  def self.all : Array(M)
  def self.all(query_string : String) : Array(M)
  def self.where(condition : String) : Array(M)
  def self.first : M
  def self.first? : M?
  def save : Bool
  def save! : Bool
  def self.delete_all
  def delete
  # Field "foo"
  def foo : T
  def foo? : T?
  def [](key) : T
  def []?(key) : T?
  def []=(key, val)
  # Aggregations
  def self.count_by_xxx : Hash(Type, Int64)
  # Misc
  def self.pluck(fields : Array(String))
API : Query
class JobName < Pon::Query
  adapter pg
  field name : String
  field size : Int32 = "length(name)"
  from <<-SQL
    FROM  jobs
    ORDER BY {{order}}
    LIMIT {{limit}}
    SQL
  def self.all(limit = 10, order = "name")
    context.bind(limit: limit, order: order).all
  end
end
JobName.all(limit: 2).map(&.name) # => ["bar", "foo"]
Connection Pool
The Adapter.database is pooled because it uses DB::Database. The default max_pool_size is 5.
It can be overridden by Setting and reset!.
Job.adapter.database.pool.stats.max_connections # => 5
Pon::Adapter::Pg.setting.max_pool_size = 3
Pon::Adapter.reset!
Job.adapter.database.pool.stats.max_connections # => 3
Logging
Pon.query_logging=(v : Bool) # writes queries into the logger or not (default: true)
Pon.query_logging?           # => true
# when Log is enabled
Pon.logger=(v : Logger)      # replace Logger instance
# when Logger is enabled
Pon.log.backend = Log::IOBackend.new(io: File.open("sql.log", "w+"))
Pon.log.level = :debug
Roadmap
- Adapter Core
- [x] connect lazily
 - [x] share same connection between adapters
 - [x] exec plain sql
 - [x] exec prepared statement
 - [x] all
 - [x] count
 - [x] scalar
 - [x] quote
 - [ ] escape
 - [x] migrator
 
 - Adapter Drivers
- [x] reset connections
 - RDB
- [x] mysql
 - [x] pg
 - [x] sqlite
 
 - KVS
- [ ] redis
 
 
 - Core
- [x] pluralize table names
 - [x] special types (Enum)
 - [ ] custom type
 - [x] multibytes
 - [x] record status
 - [x] inspect class and records
 - [ ] callbacks
 - [ ] validations
 - [x] natural keys
 - [x] default values
 
 - CRUD
- [x] create
 - [x] delete, delete_all
 - [x] find
 - [x] save
 
 - Finder
- [x] all, count, first
 - [x] pluck (with casting)
 - [ ] field aliases
 
 - Aggregations
- [x] count_by_xxx
 
 - Relations
- [ ] belongs_to
 - [ ] has_many
 - [ ] has_many through
 
 - Query
- [x] typed column access
 - [x] arbitrary select query
 - [x] prepared statement
 
 - Misc
- [ ] bulk insert
 - [ ] upsert
 
 
Installation
Add this to your application's shard.yml:
dependencies:
  pon:
    github: maiha/pon.cr
    version: 1.6.0
  # one of following adapter
  mysql:
    github: crystal-lang/crystal-mysql
  sqlite3:
    github: crystal-lang/crystal-sqlite3
  pg:
    github: will/crystal-pg
Development
$ make spec
Contributing
- Fork it ( https://github.com/maiha/pon.cr/fork )
 - Create your feature branch (git checkout -b my-new-feature)
 - Commit your changes (git commit -am 'Add some feature')
 - Push to the branch (git push origin my-new-feature)
 - Create a new Pull Request
 
Contributors
- maiha maiha - creator, maintainer