rasti-db 0.1.0

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