Rasti::DB

Gem Version CI Coverage Status Code Climate

Database collections and relations

Installation

Add this line to your application's Gemfile:

gem 'rasti-db'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rasti-db

Usage

Database connection

DB_1 = Sequel.connect ...
DB_2 = Sequel.connect ...

Database schema

DB_1.create_table :users do
  primary_key :id
  String :name, null: false, unique: true
end

DB_1.create_table :posts do
  primary_key :id
  String :title, null: false, unique: true
  String :body, null: false
  Integer :language_id, null: false, index: true
  foreign_key :user_id, :users, null: false, index: true
end

DB_1.create_table :comments do
  primary_key :id
  String :text, null: false
  foreign_key :user_id, :users, null: false, index: true
  foreign_key :post_id, :posts, null: false, index: true
end

DB_1.create_table :categories do
  primary_key :id
  String :name, null: false, unique: true
end

DB_1.create_table :categories_posts do
  foreign_key :category_id, :categories, null: false, index: true
  foreign_key :post_id, :posts, null: false, index: true
  primary_key [:category_id, :post_id]
end

DB_1.create_table :people do
  String :document_number, null: false, primary_key: true
  String :first_name, null: false
  String :last_name, null: false
  Date :birth_date, null: false
  foreign_key :user_id, :users, null: false, unique: true
end

DB_1.create_table :languages_people do
  Integer :language_id, null: false, index: true
  foreign_key :document_number, :people, type: String, null: false, index: true
  primary_key [:language_id, :document_number]
end

DB_2.create_table :languages do
  primary_key :id
  String :name, null: false, unique: true
end

Models

User     = Rasti::DB::Model[:id, :name, :posts, :comments, :person]
Post     = Rasti::DB::Model[:id, :title, :body, :user_id, :user, :comments, :categories]
Comment  = Rasti::DB::Model[:id, :text, :user_id, :user, :post_id, :post]
Category = Rasti::DB::Model[:id, :name, :posts]
Person   = Rasti::DB::Model[:document_number, :first_name, :last_name, :full_name, :birth_date, :user_id, :user]
Language = Rasti::DB::Model[:id, :name, :people]

Collections

class Users < Rasti::DB::Collection
  one_to_many :posts
  one_to_many :comments
  one_to_one :person
end

class Posts < Rasti::DB::Collection
  many_to_one :user
  many_to_many :categories
  one_to_many :comments

  query :created_by do |user_id|
    where user_id: user_id
  end

  query :entitled, -> (title) { where title: title }

  query :commented_by do |user_id|
    chainable do
      dataset.join(qualify(:comments), post_id: :id)
             .where(Sequel[:comments][:user_id] => user_id)
             .select_all(:posts)
             .distinct
    end
  end
end

class Comments < Rasti::DB::Collection
  many_to_one :user
  many_to_one :post
end

class Categories < Rasti::DB::Collection
  many_to_many :posts
end

class People < Rasti::DB::Collection
  set_collection_name :people
  set_primary_key :document_number
  set_foreign_key :document_number
  set_model Person

  many_to_one :user
  many_to_many :languages

  computed_attribute :full_name do
    Rasti::DB::ComputedAttribute.new Sequel.join([:first_name, ' ', :last_name])
  end
end

class Languages < Rasti::DB::Collection
  set_data_source_name :other

  one_to_many :posts
  many_to_many :people, collection: People, relation_data_source_name: :default
end

environment = Rasti::DB::Environment.new default: Rasti::DB::DataSource.new(DB_1),
                                         other: Rasti::DB::DataSource.new(DB_2, 'custom_schema')

users      = Users.new environment
posts      = Posts.new environment
comments   = Comments.new environment
categories = Categories.new environment
people     = People.new environment

Persistence

DB_1.transaction do
  id = users.insert name: 'User 1'
  users.update id, name: 'User updated'
  users.delete id

  users.bulk_insert [{name: 'User 1'}, {name: 'User 2'}]
  users.bulk_update(name: 'User updated') { where id: [1,2] }
  users.bulk_delete { where id: [1,2] }

  posts.insert_relations 1, categories: [2,3]
  posts.delete_relations 1, categories: [2,3]
end

Queries

posts.all # => [Post, ...]
posts.first # => Post
posts.count # => 1

posts.where(id: [1,2]) # => [Post, ...]
posts.where{id > 1}.limit(10).offset(20) } # => [Post, ...]

posts.where(id: [1,2]).raw # => [{id:1, ...}, {id:2, ...}]
posts.where(id: [1,2]).primary_keys # => [1,2]
posts.where(id: [1,2]).pluck(:id) # => [1,2]
posts.where(id: [1,2]).pluck(:id, :title) # => [[1, ...], [2, ...]]

posts.created_by(1) # => [Post, ...]
posts.created_by(1).entitled('...').commented_by(2) # => [Post, ...]
posts.with_categories([1,2]) # => [Post, ...]

posts.graph(:user, :language, :categories, 'comments.user') # => [Post(User, Language, [Categories, ...], [Comments(User)]), ...]

posts.join(:user).where(name: 'User 4') # => [Post, ...]

posts.select_attributes(:id, :title) # => [Post, ...]
posts.exclude_attributes(:id, :title) # => [Post, ...]
posts.all_attributes # => [Post, ...]

posts.graph('user.person').select_graph_attributes(user: [:id], 'user.person': [:last_name, :user_id]) # => [Post, ...]
posts.graph('user.person').exclude_graph_attributes(user: [:name], 'user.person': [:first_name, :last_name]) # => [Post, ...]
posts.graph('user.person').all_graph_attributes('user.person') # => [Post, ...]

posts.each { |post| do_something post } # Iterate posts loading all at first
posts.each(batch_size: 1000) { |post| do_something post } # Iterate posts loading in batches
posts.each_batch(size: 1000) { |posts| do_something posts } # Iterate batches of posts

Natural Query Language

posts.nql('id = 1') # => Equal
posts.nql('id != 1') # => Not equal
posts.nql('title: My post') # => Include
posts.nql('title !: My post') # => Not include
posts.nql('title ~ My post') # => Insensitive like
posts.nql('id > 1') # => Greater
posts.nql('id >= 1') # => Greater or equal
posts.nql('id < 10') # => Less
posts.nql('id <= 10') # => Less or equal

posts.nql('id = 1 | id = 2') # => Or
posts.nql('id > 1 & title: "My post"') # => And
posts.nql('(id > 3 & id < 10) | title: "My post"') # => Precedence

posts.nql('comments.user.person.document_number = 7') # => Nested

Development

Rasti::DB uses treetop to perform queries using natural language. To recompile the syntax, simply run the following command in lib/rasti/db/nql:

tt syntax.treetop -o syntax.rb

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/gabynaiman/rasti-db.

License

The gem is available as open source under the terms of the MIT License.