rasti-db 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +2 -0
- data/.gitignore +9 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +11 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +161 -0
- data/Rakefile +25 -0
- data/lib/rasti/db/collection.rb +263 -0
- data/lib/rasti/db/helpers.rb +18 -0
- data/lib/rasti/db/model.rb +105 -0
- data/lib/rasti/db/query.rb +113 -0
- data/lib/rasti/db/relations.rb +159 -0
- data/lib/rasti/db/type_converter.rb +52 -0
- data/lib/rasti/db/version.rb +5 -0
- data/lib/rasti/db.rb +10 -0
- data/lib/rasti-db.rb +1 -0
- data/rasti-db.gemspec +44 -0
- data/spec/collection_spec.rb +494 -0
- data/spec/coverage_helper.rb +5 -0
- data/spec/minitest_helper.rb +113 -0
- data/spec/model_spec.rb +82 -0
- data/spec/query_spec.rb +122 -0
- data/spec/relations_spec.rb +144 -0
- data/spec/type_converter_spec.rb +98 -0
- metadata +231 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
module Rasti
|
2
|
+
module DB
|
3
|
+
class Query
|
4
|
+
|
5
|
+
DATASET_CHAINED_METHODS = [:where, :exclude, :and, :or, :order, :reverse_order, :limit, :offset].freeze
|
6
|
+
|
7
|
+
include Enumerable
|
8
|
+
include Helpers::WithSchema
|
9
|
+
|
10
|
+
def initialize(collection_class, dataset, relations=[], schema=nil)
|
11
|
+
@collection_class = collection_class
|
12
|
+
@dataset = dataset
|
13
|
+
@relations = relations
|
14
|
+
@schema = schema
|
15
|
+
end
|
16
|
+
|
17
|
+
def raw
|
18
|
+
dataset.all
|
19
|
+
end
|
20
|
+
|
21
|
+
def pluck(*attributes)
|
22
|
+
ds = dataset.select(*attributes.map { |attr| Sequel.qualify(collection_class.collection_name, attr) })
|
23
|
+
attributes.count == 1 ? ds.map { |r| r[attributes.first] } : ds.map(&:values)
|
24
|
+
end
|
25
|
+
|
26
|
+
def primary_keys
|
27
|
+
pluck collection_class.primary_key
|
28
|
+
end
|
29
|
+
|
30
|
+
def all
|
31
|
+
with_relations(dataset.all).map do |row|
|
32
|
+
collection_class.model.new row
|
33
|
+
end
|
34
|
+
end
|
35
|
+
alias_method :to_a, :all
|
36
|
+
|
37
|
+
def each(&block)
|
38
|
+
all.each &block
|
39
|
+
end
|
40
|
+
|
41
|
+
DATASET_CHAINED_METHODS.each do |method|
|
42
|
+
define_method method do |*args, &block|
|
43
|
+
Query.new collection_class,
|
44
|
+
dataset.send(method, *args, &block),
|
45
|
+
relations,
|
46
|
+
schema
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def graph(*rels)
|
51
|
+
Query.new collection_class,
|
52
|
+
dataset,
|
53
|
+
(relations | rels),
|
54
|
+
schema
|
55
|
+
end
|
56
|
+
|
57
|
+
def count
|
58
|
+
dataset.count
|
59
|
+
end
|
60
|
+
|
61
|
+
def any?
|
62
|
+
count > 0
|
63
|
+
end
|
64
|
+
|
65
|
+
def empty?
|
66
|
+
!any?
|
67
|
+
end
|
68
|
+
|
69
|
+
def first
|
70
|
+
instance = with_relations dataset.first
|
71
|
+
instance ? collection_class.model.new(instance) : nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def last
|
75
|
+
instance = with_relations dataset.last
|
76
|
+
instance ? collection_class.model.new(instance) : nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
"#<#{self.class.name}: \"#{dataset.sql}\">"
|
81
|
+
end
|
82
|
+
alias_method :inspect, :to_s
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def chainable(&block)
|
87
|
+
ds = instance_eval &block
|
88
|
+
Query.new collection_class, ds, relations, schema
|
89
|
+
end
|
90
|
+
|
91
|
+
def with_relations(data)
|
92
|
+
rows = data.is_a?(Array) ? data : [data]
|
93
|
+
Relations.graph_to rows, relations, collection_class, dataset.db, schema
|
94
|
+
data
|
95
|
+
end
|
96
|
+
|
97
|
+
def method_missing(method, *args, &block)
|
98
|
+
if collection_class.queries.key?(method)
|
99
|
+
instance_exec *args, &collection_class.queries[method]
|
100
|
+
else
|
101
|
+
super
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def respond_to_missing?(method, include_private=false)
|
106
|
+
collection_class.queries.key?(method) || super
|
107
|
+
end
|
108
|
+
|
109
|
+
attr_reader :collection_class, :dataset, :relations, :schema
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Rasti
|
2
|
+
module DB
|
3
|
+
module Relations
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def graph_to(rows, relations, collection_class, db, schema=nil)
|
8
|
+
return if rows.empty?
|
9
|
+
|
10
|
+
parse(relations).each do |relation, nested_relations|
|
11
|
+
raise "Undefined relation #{relation} for #{collection_class}" unless collection_class.relations.key? relation
|
12
|
+
collection_class.relations[relation].graph_to rows, db, schema, nested_relations
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def parse(relations)
|
19
|
+
relations.each_with_object({}) do |relation, hash|
|
20
|
+
tail = relation.to_s.split '.'
|
21
|
+
head = tail.shift.to_sym
|
22
|
+
hash[head] ||= []
|
23
|
+
hash[head] << tail.join('.') unless tail.empty?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
class Base
|
31
|
+
|
32
|
+
include Sequel::Inflections
|
33
|
+
|
34
|
+
attr_reader :name, :source_collection_class
|
35
|
+
|
36
|
+
def initialize(name, source_collection_class, options={})
|
37
|
+
@name = name
|
38
|
+
@source_collection_class = source_collection_class
|
39
|
+
@options = options
|
40
|
+
end
|
41
|
+
|
42
|
+
def target_collection_class
|
43
|
+
@target_collection_class ||= @options[:collection].is_a?(Class) ? @options[:collection] : Consty.get(@options[:collection] || camelize(pluralize(name)), source_collection_class)
|
44
|
+
end
|
45
|
+
|
46
|
+
def one_to_many?
|
47
|
+
is_a? OneToMany
|
48
|
+
end
|
49
|
+
|
50
|
+
def many_to_one?
|
51
|
+
is_a? ManyToOne
|
52
|
+
end
|
53
|
+
|
54
|
+
def many_to_many?
|
55
|
+
is_a? ManyToMany
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
attr_reader :options
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
class OneToMany < Base
|
66
|
+
|
67
|
+
def foreign_key
|
68
|
+
@foreign_key ||= @options[:foreign_key] || source_collection_class.implicit_foreign_key_name
|
69
|
+
end
|
70
|
+
|
71
|
+
def graph_to(rows, db, schema=nil, relations=[])
|
72
|
+
pks = rows.map { |row| row[source_collection_class.primary_key] }.uniq
|
73
|
+
|
74
|
+
target_collection = target_collection_class.new db, schema
|
75
|
+
|
76
|
+
relation_rows = target_collection.where(foreign_key => pks)
|
77
|
+
.graph(*relations)
|
78
|
+
.group_by { |r| r.public_send(foreign_key) }
|
79
|
+
|
80
|
+
rows.each do |row|
|
81
|
+
row[name] = relation_rows.fetch row[source_collection_class.primary_key], []
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
class ManyToOne < Base
|
89
|
+
|
90
|
+
def foreign_key
|
91
|
+
@foreign_key ||= @options[:foreign_key] || target_collection_class.implicit_foreign_key_name
|
92
|
+
end
|
93
|
+
|
94
|
+
def graph_to(rows, db, schema=nil, relations=[])
|
95
|
+
fks = rows.map { |row| row[foreign_key] }.uniq
|
96
|
+
|
97
|
+
target_collection = target_collection_class.new db, schema
|
98
|
+
|
99
|
+
relation_rows = target_collection.where(source_collection_class.primary_key => fks)
|
100
|
+
.graph(*relations)
|
101
|
+
.each_with_object({}) do |row, hash|
|
102
|
+
hash[row.public_send(source_collection_class.primary_key)] = row
|
103
|
+
end
|
104
|
+
|
105
|
+
rows.each do |row|
|
106
|
+
row[name] = relation_rows[row[foreign_key]]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
class ManyToMany < Base
|
114
|
+
|
115
|
+
def source_foreign_key
|
116
|
+
@source_foreign_key ||= @options[:source_foreign_key] || source_collection_class.implicit_foreign_key_name
|
117
|
+
end
|
118
|
+
|
119
|
+
def target_foreign_key
|
120
|
+
@target_foreign_key ||= @options[:target_foreign_key] || target_collection_class.implicit_foreign_key_name
|
121
|
+
end
|
122
|
+
|
123
|
+
def relation_collection_name
|
124
|
+
@relation_collection_name ||= @options[:relation_collection_name] || [source_collection_class.collection_name, target_collection_class.collection_name].sort.join('_').to_sym
|
125
|
+
end
|
126
|
+
|
127
|
+
def qualified_relation_collection_name(schema=nil)
|
128
|
+
schema.nil? ? relation_collection_name : Sequel.qualify(schema, relation_collection_name)
|
129
|
+
end
|
130
|
+
|
131
|
+
def graph_to(rows, db, schema=nil, relations=[])
|
132
|
+
pks = rows.map { |row| row[source_collection_class.primary_key] }
|
133
|
+
|
134
|
+
target_collection = target_collection_class.new db, schema
|
135
|
+
|
136
|
+
relation_name = qualified_relation_collection_name schema
|
137
|
+
|
138
|
+
join_rows = target_collection.dataset
|
139
|
+
.join(relation_name, target_foreign_key => target_collection_class.primary_key)
|
140
|
+
.where(Sequel.qualify(relation_name, source_foreign_key) => pks)
|
141
|
+
.all
|
142
|
+
|
143
|
+
Relations.graph_to join_rows, relations, target_collection_class, db, schema
|
144
|
+
|
145
|
+
relation_rows = join_rows.each_with_object(Hash.new { |h,k| h[k] = [] }) do |row, hash|
|
146
|
+
attributes = row.select { |attr,_| target_collection_class.model.attributes.include? attr }
|
147
|
+
hash[row[source_foreign_key]] << target_collection_class.model.new(attributes)
|
148
|
+
end
|
149
|
+
|
150
|
+
rows.each do |row|
|
151
|
+
row[name] = relation_rows.fetch row[target_collection_class.primary_key], []
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Rasti
|
2
|
+
module DB
|
3
|
+
class TypeConverter
|
4
|
+
|
5
|
+
CONVERTIONS = {
|
6
|
+
postgres: {
|
7
|
+
/^json$/ => ->(value, match) { Sequel.pg_json value },
|
8
|
+
/^hstore$/ => ->(value, match) { Sequel.hstore value },
|
9
|
+
/^hstore\[\]$/ => ->(value, match) { Sequel.pg_array value.map { |v| Sequel.hstore v }, 'hstore' },
|
10
|
+
/^([a-z]+)\[\]$/ => ->(value, match) { Sequel.pg_array value, match.captures[0] }
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
def initialize(db, collection_name)
|
15
|
+
@db = db
|
16
|
+
@collection_name = collection_name
|
17
|
+
end
|
18
|
+
|
19
|
+
def apply_to(attributes)
|
20
|
+
convertions = self.class.convertions_for @db, @collection_name
|
21
|
+
|
22
|
+
return attributes if convertions.empty?
|
23
|
+
|
24
|
+
(attributes.is_a?(Array) ? attributes : [attributes]).each do |attrs|
|
25
|
+
convertions.each do |name, convertion|
|
26
|
+
attrs[name] = convertion[:block].call attrs[name], convertion[:match] if attrs.key? name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
attributes
|
31
|
+
end
|
32
|
+
|
33
|
+
@cache ||= {}
|
34
|
+
|
35
|
+
def self.convertions_for(db, collection_name)
|
36
|
+
key = [db.database_type, collection_name]
|
37
|
+
if !@cache.key?(key)
|
38
|
+
columns = Hash[db.schema(collection_name)]
|
39
|
+
@cache[key] = columns.each_with_object({}) do |(name, schema), hash|
|
40
|
+
CONVERTIONS.fetch(db.database_type, {}).each do |type, convertion|
|
41
|
+
if !hash.key?(name) && match = type.match(schema[:db_type])
|
42
|
+
hash[name] = {match: match, block: convertion}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
@cache[key]
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/rasti/db.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'consty'
|
3
|
+
|
4
|
+
require_relative 'db/version'
|
5
|
+
require_relative 'db/helpers'
|
6
|
+
require_relative 'db/query'
|
7
|
+
require_relative 'db/collection'
|
8
|
+
require_relative 'db/model'
|
9
|
+
require_relative 'db/relations'
|
10
|
+
require_relative 'db/type_converter'
|
data/lib/rasti-db.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'rasti/db'
|
data/rasti-db.gemspec
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rasti/db/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'rasti-db'
|
8
|
+
spec.version = Rasti::DB::VERSION
|
9
|
+
spec.authors = ['Gabriel Naiman']
|
10
|
+
spec.email = ['gabynaiman@gmail.com']
|
11
|
+
spec.summary = 'Database collections and relations'
|
12
|
+
spec.description = 'Database collections and relations'
|
13
|
+
spec.homepage = 'https://github.com/gabynaiman/rasti-db'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'sequel', '~> 4.38'
|
22
|
+
spec.add_runtime_dependency 'consty', '~> 1.0'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.12'
|
25
|
+
spec.add_development_dependency 'rake', '~> 11.0'
|
26
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
27
|
+
spec.add_development_dependency 'minitest-colorin', '~> 0.1'
|
28
|
+
spec.add_development_dependency 'minitest-line', '~> 0.6'
|
29
|
+
spec.add_development_dependency 'simplecov', '~> 0.12'
|
30
|
+
spec.add_development_dependency 'coveralls', '~> 0.8'
|
31
|
+
spec.add_development_dependency 'pry-nav', '~> 0.2'
|
32
|
+
|
33
|
+
if RUBY_ENGINE == 'jruby'
|
34
|
+
spec.add_development_dependency 'jdbc-sqlite3', '~> 3.8'
|
35
|
+
else
|
36
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
37
|
+
end
|
38
|
+
|
39
|
+
if RUBY_VERSION < '2'
|
40
|
+
spec.add_development_dependency 'term-ansicolor', '~> 1.3.0'
|
41
|
+
spec.add_development_dependency 'tins', '~> 1.6.0'
|
42
|
+
spec.add_development_dependency 'json', '~> 1.8'
|
43
|
+
end
|
44
|
+
end
|