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 +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
|