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.
@@ -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
@@ -0,0 +1,5 @@
1
+ module Rasti
2
+ module DB
3
+ VERSION = '0.1.0'
4
+ end
5
+ 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