rasti-db 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fa66e94f3945ca058846ce6031b4720516d58646
4
+ data.tar.gz: 87e086acc3972e6a553aea23e9741a8d74235022
5
+ SHA512:
6
+ metadata.gz: 6d0add21909af7df516237b904484e65b68042ec9da5f792ecc4c517a63d175fc98a19389ee2ca063f8a028457e9fd0adab0c2f4a46ea323ea93ab3148be8030
7
+ data.tar.gz: db6e89b5c05617e65aaa07dab882de5376acf0dd3bda0d94001eaf5e24f482772b1fb3d8617c8647aeb72e26aeb5d62ccf15706ab3ea6617872adc0bccc696e7
data/.coveralls.yml ADDED
@@ -0,0 +1,2 @@
1
+ service_name: travis-ci
2
+ repo_token: jTZIGdJqN5w1Kph0muxBAXaxDr0C9VZAP
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ rasti-db
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.3.0
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0
5
+ - 2.1
6
+ - 2.2
7
+ - 2.3.0
8
+ - 2.4.0
9
+ - jruby
10
+ before_install:
11
+ - gem install bundler
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rasti-db.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Gabriel Naiman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # Rasti::DB
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/rasti-db.svg)](https://rubygems.org/gems/rasti-db)
4
+ [![Build Status](https://travis-ci.org/gabynaiman/rasti-db.svg?branch=master)](https://travis-ci.org/gabynaiman/rasti-db)
5
+ [![Coverage Status](https://coveralls.io/repos/github/gabynaiman/rasti-db/badge.svg?branch=master)](https://coveralls.io/github/gabynaiman/rasti-db?branch=master)
6
+ [![Code Climate](https://codeclimate.com/github/gabynaiman/rasti-db.svg)](https://codeclimate.com/github/gabynaiman/rasti-db)
7
+ [![Dependency Status](https://gemnasium.com/gabynaiman/rasti-db.svg)](https://gemnasium.com/gabynaiman/rasti-db)
8
+
9
+ Database collections and relations
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'rasti-db'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install rasti-db
26
+
27
+ ## Usage
28
+
29
+ ### Database connection
30
+
31
+ ```ruby
32
+ DB = Sequel.connect ...
33
+ ```
34
+
35
+ ### Database schema
36
+
37
+ ```ruby
38
+ DB.create_table :users do
39
+ primary_key :id
40
+ String :name, null: false, unique: true
41
+ end
42
+
43
+ DB.create_table :posts do
44
+ primary_key :id
45
+ String :title, null: false, unique: true
46
+ String :body, null: false
47
+ foreign_key :user_id, :users, null: false, index: true
48
+ end
49
+
50
+ DB.create_table :comments do
51
+ primary_key :id
52
+ String :text, null: false
53
+ foreign_key :user_id, :users, null: false, index: true
54
+ foreign_key :post_id, :posts, null: false, index: true
55
+ end
56
+
57
+ DB.create_table :categories do
58
+ primary_key :id
59
+ String :name, null: false, unique: true
60
+ end
61
+
62
+ DB.create_table :categories_posts do
63
+ foreign_key :category_id, :categories, null: false, index: true
64
+ foreign_key :post_id, :posts, null: false, index: true
65
+ primary_key [:category_id, :post_id]
66
+ end
67
+ ```
68
+
69
+ ### Models
70
+
71
+ ```ruby
72
+ User = Rasti::DB::Model[:id, :name, :posts, :comments]
73
+ Post = Rasti::DB::Model[:id, :title, :body, :user_id, :user, :comments, :categories]
74
+ Comment = Rasti::DB::Model[:id, :text, :user_id, :user, :post_id, :post]
75
+ Category = Rasti::DB::Model[:id, :name, :posts]
76
+ ```
77
+
78
+ ### Collections
79
+
80
+ ```ruby
81
+ class Users < Rasti::DB::Collection
82
+ one_to_many :posts
83
+ one_to_many :comments
84
+ end
85
+
86
+ class Posts < Rasti::DB::Collection
87
+ many_to_one :user
88
+ many_to_many :categories
89
+ one_to_many :comments
90
+
91
+ query :created_by do |user_id|
92
+ where user_id: user_id
93
+ end
94
+
95
+ query :entitled, -> (title) { where title: title }
96
+
97
+ query :commented_by do |user_id|
98
+ chainable do
99
+ dataset.join(with_schema(:comments), post_id: :id)
100
+ .where(with_schema(:comments, :user_id) => user_id)
101
+ .select_all(with_schema(:posts))
102
+ .distinct
103
+ end
104
+ end
105
+ end
106
+
107
+ class Comments < Rasti::DB::Collection
108
+ many_to_one :user
109
+ many_to_one :post
110
+ end
111
+
112
+ class Categories < Rasti::DB::Collection
113
+ many_to_many :posts
114
+ end
115
+
116
+ users = Users.new DB
117
+ posts = Posts.new DB
118
+ comments = Comments.new DB
119
+ categories = Categories.new DB
120
+ ```
121
+
122
+ ### Persistence
123
+
124
+ ```ruby
125
+ DB.transaction do
126
+ id = users.insert name: 'User 1'
127
+ users.update id, name: 'User updated'
128
+ users.delete id
129
+
130
+ users.bulk_insert [{name: 'User 1'}, {name: 'User 2'}]
131
+ users.bulk_update(name: 'User updated') { where id: [1,2] }
132
+ users.bulk_delete { where id: [1,2] }
133
+ end
134
+ ```
135
+
136
+ ### Queries
137
+
138
+ ```ruby
139
+ posts.all # => [Post, ...]
140
+ posts.first # => Post
141
+ posts.count # => 1
142
+ posts.where(id: [1,2]) # => [Post, ...]
143
+ posts.where{id > 1}.limit(10).offset(20) } # => [Post, ...]
144
+ posts.graph(:user, :categories, 'comments.user') # => [Post(User, [Categories, ...], [Comments(User)]), ...]
145
+ posts.created_by(1) # => [Post, ...]
146
+ posts.created_by(1).entitled('...').commented_by(2) # => [Post, ...]
147
+ posts.where(id: [1,2]).raw # => [{id:1, ...}, {id:2, ...}]
148
+ posts.where(id: [1,2]).primary_keys # => [1,2]
149
+ posts.where(id: [1,2]).pluck(:id) # => [1,2]
150
+ posts.where(id: [1,2]).pluck(:id, :title) # => [[1, ...], [2, ...]]
151
+ ```
152
+
153
+ ## Contributing
154
+
155
+ Bug reports and pull requests are welcome on GitHub at https://github.com/gabynaiman/rasti-db.
156
+
157
+
158
+ ## License
159
+
160
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
161
+
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:spec) do |t|
5
+ t.libs << 'spec'
6
+ t.libs << 'lib'
7
+ t.pattern = ENV['DIR'] ? File.join(ENV['DIR'], '**', '*_spec.rb') : 'spec/**/*_spec.rb'
8
+ t.verbose = false
9
+ t.warning = false
10
+ t.loader = nil if ENV['TEST']
11
+ ENV['TEST'], ENV['LINE'] = ENV['TEST'].split(':') if ENV['TEST'] && !ENV['LINE']
12
+ t.options = ''
13
+ t.options << "--name=/#{ENV['NAME']}/ " if ENV['NAME']
14
+ t.options << "-l #{ENV['LINE']} " if ENV['LINE'] && ENV['TEST']
15
+ end
16
+
17
+ task default: :spec
18
+
19
+ desc 'Pry console'
20
+ task :console do
21
+ require 'rasti-db'
22
+ require 'pry'
23
+ ARGV.clear
24
+ Pry.start
25
+ end
@@ -0,0 +1,263 @@
1
+ module Rasti
2
+ module DB
3
+ class Collection
4
+
5
+ QUERY_METHODS = (Query::DATASET_CHAINED_METHODS + [:graph, :count, :all, :first]).freeze
6
+
7
+ include Helpers::WithSchema
8
+
9
+ class << self
10
+
11
+ include Sequel::Inflections
12
+
13
+ def collection_name
14
+ @collection_name ||= implicit_collection_name
15
+ end
16
+
17
+ def primary_key
18
+ @primary_key ||= :id
19
+ end
20
+
21
+ def model
22
+ if @model.is_a? Class
23
+ @model
24
+ elsif @model
25
+ @model = Consty.get @model, self
26
+ else
27
+ @model = Consty.get demodulize(singularize(name)), self
28
+ end
29
+ end
30
+
31
+ def relations
32
+ @relations ||= {}
33
+ end
34
+
35
+ def queries
36
+ @queries ||= {}
37
+ end
38
+
39
+ def implicit_collection_name
40
+ underscore(demodulize(name)).to_sym
41
+ end
42
+
43
+ def implicit_foreign_key_name
44
+ "#{singularize(collection_name)}_id".to_sym
45
+ end
46
+
47
+ private
48
+
49
+ def set_collection_name(collection_name)
50
+ @collection_name = collection_name.to_sym
51
+ end
52
+
53
+ def set_primary_key(primary_key)
54
+ @primary_key = primary_key
55
+ end
56
+
57
+ def set_model(model)
58
+ @model = model
59
+ end
60
+
61
+ def one_to_many(name, options={})
62
+ relations[name] = Relations::OneToMany.new name, self, options
63
+ end
64
+
65
+ def many_to_one(name, options={})
66
+ relations[name] = Relations::ManyToOne.new name, self, options
67
+ end
68
+
69
+ def many_to_many(name, options={})
70
+ relations[name] = Relations::ManyToMany.new name, self, options
71
+ end
72
+
73
+ def query(name, lambda=nil, &block)
74
+ queries[name] = lambda || block
75
+
76
+ define_method name do |*args|
77
+ query.instance_exec *args, &self.class.queries[name]
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+ attr_reader :db, :schema
84
+
85
+ def initialize(db, schema=nil)
86
+ @db = db
87
+ @schema = schema
88
+ end
89
+
90
+ def dataset
91
+ db[qualified_collection_name]
92
+ end
93
+
94
+ def insert(attributes)
95
+ db.transaction do
96
+ db_attributes = type_converter.apply_to attributes
97
+ collection_attributes, relations_primary_keys = split_related_attributes db_attributes
98
+ primary_key = dataset.insert collection_attributes
99
+ save_relations primary_key, relations_primary_keys
100
+ primary_key
101
+ end
102
+ end
103
+
104
+ def bulk_insert(attributes, options={})
105
+ db_attributes = type_converter.apply_to attributes
106
+ dataset.multi_insert db_attributes, options
107
+ end
108
+
109
+ def insert_relations(primary_key, relations)
110
+ relations.each do |relation_name, relation_primary_keys|
111
+ relation = self.class.relations[relation_name]
112
+ insert_relation_table relation, primary_key, relation_primary_keys
113
+ end
114
+ nil
115
+ end
116
+
117
+ def update(primary_key, attributes)
118
+ db.transaction do
119
+ db_attributes = type_converter.apply_to attributes
120
+ collection_attributes, relations_primary_keys = split_related_attributes db_attributes
121
+ dataset.where(self.class.primary_key => primary_key).update(collection_attributes) unless collection_attributes.empty?
122
+ save_relations primary_key, relations_primary_keys
123
+ end
124
+ nil
125
+ end
126
+
127
+ def bulk_update(attributes, &block)
128
+ db_attributes = type_converter.apply_to attributes
129
+ build_query(&block).instance_eval { dataset.update db_attributes }
130
+ nil
131
+ end
132
+
133
+ def delete(primary_key)
134
+ dataset.where(self.class.primary_key => primary_key).delete
135
+ nil
136
+ end
137
+
138
+ def bulk_delete(&block)
139
+ build_query(&block).instance_eval { dataset.delete }
140
+ nil
141
+ end
142
+
143
+ def delete_relations(primary_key, relations)
144
+ db.transaction do
145
+ relations.each do |relation_name, relation_primary_keys|
146
+ relation = self.class.relations[relation_name]
147
+ delete_relation_table relation, primary_key, relation_primary_keys
148
+ end
149
+ end
150
+ nil
151
+ end
152
+
153
+ def delete_cascade(*primary_keys)
154
+ db.transaction do
155
+ delete_cascade_relations primary_keys
156
+ bulk_delete { |q| q.where self.class.primary_key => primary_keys }
157
+ end
158
+ nil
159
+ end
160
+
161
+ def find(primary_key)
162
+ where(self.class.primary_key => primary_key).first
163
+ end
164
+
165
+ def find_graph(primary_key, *relations)
166
+ where(self.class.primary_key => primary_key).graph(*relations).first
167
+ end
168
+
169
+ def query
170
+ Query.new self.class, dataset, [], schema
171
+ end
172
+
173
+ QUERY_METHODS.each do |method|
174
+ define_method method do |*args, &block|
175
+ query.public_send method, *args, &block
176
+ end
177
+ end
178
+
179
+ def exists?(filter=nil, &block)
180
+ build_query(filter, &block).count > 0
181
+ end
182
+
183
+ def detect(filter=nil, &block)
184
+ build_query(filter, &block).first
185
+ end
186
+
187
+ private
188
+
189
+ def type_converter
190
+ @type_converter ||= TypeConverter.new db, qualified_collection_name
191
+ end
192
+
193
+ def qualified_collection_name
194
+ schema.nil? ? self.class.collection_name : Sequel.qualify(schema, self.class.collection_name)
195
+ end
196
+
197
+ def build_query(filter=nil, &block)
198
+ raise ArgumentError, 'must specify filter hash or block' if filter.nil? && block.nil?
199
+ if filter
200
+ query.where filter
201
+ else
202
+ block.arity == 0 ? query.instance_eval(&block) : block.call(query)
203
+ end
204
+ end
205
+
206
+ def split_related_attributes(attributes)
207
+ relation_names = self.class.relations.values.select(&:many_to_many?).map(&:name)
208
+
209
+ collection_attributes = attributes.reject { |n,v| relation_names.include? n }
210
+ relations_ids = attributes.select { |n,v| relation_names.include? n }
211
+
212
+ [collection_attributes, relations_ids]
213
+ end
214
+
215
+ def save_relations(primary_key, relations_primary_keys)
216
+ relations_primary_keys.each do |relation_name, relation_primary_keys|
217
+ relation = self.class.relations[relation_name]
218
+ delete_relation_table relation, [primary_key]
219
+ insert_relation_table relation, primary_key, relation_primary_keys
220
+ end
221
+ end
222
+
223
+ def delete_cascade_relations(primary_keys)
224
+ relations = self.class.relations.values
225
+
226
+ relations.select(&:many_to_many?).each do |relation|
227
+ delete_relation_table relation, primary_keys
228
+ end
229
+
230
+ relations.select(&:one_to_many?).each do |relation|
231
+ relation_collection_name = with_schema(relation.target_collection_class.collection_name)
232
+ relations_ids = db[relation_collection_name].where(relation.foreign_key => primary_keys)
233
+ .select(relation.target_collection_class.primary_key)
234
+ .map(relation.target_collection_class.primary_key)
235
+
236
+ target_collection = relation.target_collection_class.new db, schema
237
+ target_collection.delete_cascade *relations_ids unless relations_ids.empty?
238
+ end
239
+ end
240
+
241
+ def insert_relation_table(relation, primary_key, relation_primary_keys)
242
+ relation_collection_name = relation.qualified_relation_collection_name(schema)
243
+
244
+ values = relation_primary_keys.map do |relation_pk|
245
+ {
246
+ relation.source_foreign_key => primary_key,
247
+ relation.target_foreign_key => relation_pk
248
+ }
249
+ end
250
+
251
+ db[relation_collection_name].multi_insert values
252
+ end
253
+
254
+ def delete_relation_table(relation, primary_keys, relation_primary_keys=nil)
255
+ relation_collection_name = relation.qualified_relation_collection_name(schema)
256
+ ds = db[relation_collection_name].where(relation.source_foreign_key => primary_keys)
257
+ ds = ds.where(relation.target_foreign_key => relation_primary_keys) if relation_primary_keys
258
+ ds.delete
259
+ end
260
+
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,18 @@
1
+ module Rasti
2
+ module DB
3
+ module Helpers
4
+
5
+ module WithSchema
6
+
7
+ private
8
+
9
+ def with_schema(table, field=nil)
10
+ qualified_table = schema ? Sequel.qualify(schema, table) : table
11
+ field ? Sequel.qualify(qualified_table, field) : qualified_table
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,105 @@
1
+ module Rasti
2
+ module DB
3
+ class Model
4
+
5
+ class UninitializedAttributeError < StandardError
6
+
7
+ attr_reader :attribute
8
+
9
+ def initialize(attribute)
10
+ @attribute = attribute
11
+ super "Uninitialized attribute #{attribute}"
12
+ end
13
+
14
+ end
15
+
16
+
17
+ class << self
18
+
19
+ def [](*attribute_names)
20
+ Class.new(self) do
21
+ attribute *attribute_names
22
+
23
+ def self.inherited(subclass)
24
+ subclass.instance_variable_set :@attributes, attributes.dup
25
+ end
26
+ end
27
+ end
28
+
29
+ def attributes
30
+ @attributes ||= []
31
+ end
32
+
33
+ def model_name
34
+ name || self.superclass.name
35
+ end
36
+
37
+ def to_s
38
+ "#{model_name}[#{attributes.join(', ')}]"
39
+ end
40
+ alias_method :inspect, :to_s
41
+
42
+ private
43
+
44
+ def attribute(*names)
45
+ names.each do |name|
46
+ raise ArgumentError, "Attribute #{name} already exists" if attributes.include?(name)
47
+
48
+ attributes << name
49
+
50
+ define_method name do
51
+ attributes.key?(name) ? attributes[name] : raise(UninitializedAttributeError, name)
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+
59
+ def initialize(attributes)
60
+ invalid_attributes = attributes.keys - self.class.attributes
61
+ raise "#{self.class.model_name} invalid attributes: #{invalid_attributes.join(', ')}" unless invalid_attributes.empty?
62
+ @attributes = attributes
63
+ end
64
+
65
+ def eql?(other)
66
+ instance_of?(other.class) && to_h.eql?(other.to_h)
67
+ end
68
+
69
+ def ==(other)
70
+ other.kind_of?(self.class) && to_h == other.to_h
71
+ end
72
+
73
+ def hash
74
+ attributes.map(&:hash).hash
75
+ end
76
+
77
+ def to_s
78
+ "#<#{self.class.model_name}[#{attributes.map { |n,v| "#{n}: #{v.inspect}" }.join(', ')}]>"
79
+ end
80
+ alias_method :inspect, :to_s
81
+
82
+ def to_h
83
+ self.class.attributes.each_with_object({}) do |name, hash|
84
+ if attributes.key? name
85
+ case attributes[name]
86
+ when Model
87
+ hash[name] = attributes[name].to_h
88
+ when Array
89
+ hash[name] = attributes[name].map do |e|
90
+ e.is_a?(Model) ? e.to_h : e
91
+ end
92
+ else
93
+ hash[name] = attributes[name]
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ attr_reader :attributes
102
+
103
+ end
104
+ end
105
+ end