rasti-db 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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