yadm 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +273 -0
- data/Rakefile +10 -0
- data/examples/basic.rb +43 -0
- data/examples/migration.rb +65 -0
- data/lib/yadm.rb +39 -0
- data/lib/yadm/adapters.rb +32 -0
- data/lib/yadm/adapters/common_sql.rb +120 -0
- data/lib/yadm/adapters/memory.rb +175 -0
- data/lib/yadm/adapters/mysql.rb +17 -0
- data/lib/yadm/adapters/postgresql.rb +17 -0
- data/lib/yadm/adapters/sqlite.rb +17 -0
- data/lib/yadm/criteria.rb +32 -0
- data/lib/yadm/criteria/argument.rb +22 -0
- data/lib/yadm/criteria/attribute.rb +15 -0
- data/lib/yadm/criteria/condition.rb +31 -0
- data/lib/yadm/criteria/expression.rb +19 -0
- data/lib/yadm/criteria/limit.rb +25 -0
- data/lib/yadm/criteria/order.rb +48 -0
- data/lib/yadm/criteria_parser.rb +62 -0
- data/lib/yadm/criteria_parser/expression_parser.rb +77 -0
- data/lib/yadm/entity.rb +53 -0
- data/lib/yadm/identity_map.rb +51 -0
- data/lib/yadm/mapper.rb +16 -0
- data/lib/yadm/mapping.rb +71 -0
- data/lib/yadm/mapping/attribute.rb +31 -0
- data/lib/yadm/query.rb +28 -0
- data/lib/yadm/repository.rb +103 -0
- data/lib/yadm/version.rb +3 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/criteria_helpers.rb +33 -0
- data/spec/support/sequel_helpers.rb +25 -0
- data/spec/support/shared_examples_for_a_sequel_adapter.rb +173 -0
- data/spec/yadm/adapters/common_sql_spec.rb +89 -0
- data/spec/yadm/adapters/memory_spec.rb +230 -0
- data/spec/yadm/adapters/mysql_spec.rb +9 -0
- data/spec/yadm/adapters/postgresql_spec.rb +9 -0
- data/spec/yadm/adapters/sqlite_spec.rb +5 -0
- data/spec/yadm/adapters_spec.rb +32 -0
- data/spec/yadm/criteria/condition_spec.rb +50 -0
- data/spec/yadm/criteria/limit_spec.rb +45 -0
- data/spec/yadm/criteria/order_spec.rb +50 -0
- data/spec/yadm/criteria_parser/expression_parser_spec.rb +47 -0
- data/spec/yadm/criteria_parser_spec.rb +55 -0
- data/spec/yadm/criteria_spec.rb +40 -0
- data/spec/yadm/entity_spec.rb +76 -0
- data/spec/yadm/identity_map_spec.rb +128 -0
- data/spec/yadm/mapper_spec.rb +23 -0
- data/spec/yadm/mapping/attribute_spec.rb +35 -0
- data/spec/yadm/mapping_spec.rb +122 -0
- data/spec/yadm/query_spec.rb +45 -0
- data/spec/yadm/repository_spec.rb +175 -0
- data/spec/yadm_spec.rb +45 -0
- data/yadm.gemspec +33 -0
- metadata +254 -0
data/lib/yadm.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'yadm/version'
|
2
|
+
require 'yadm/entity'
|
3
|
+
require 'yadm/adapters'
|
4
|
+
require 'yadm/identity_map'
|
5
|
+
require 'yadm/mapping'
|
6
|
+
require 'yadm/mapper'
|
7
|
+
require 'yadm/criteria'
|
8
|
+
require 'yadm/query'
|
9
|
+
require 'yadm/criteria_parser'
|
10
|
+
require 'yadm/repository'
|
11
|
+
|
12
|
+
module YADM
|
13
|
+
class << self
|
14
|
+
def setup(&block)
|
15
|
+
instance_eval(&block) unless block.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def data_source(name, adapter:, **connection_params)
|
19
|
+
data_source = Adapters.fetch(adapter).new(connection_params)
|
20
|
+
data_sources[name] = IdentityMap.new(data_source)
|
21
|
+
end
|
22
|
+
|
23
|
+
def map(&block)
|
24
|
+
mapper.instance_eval(&block) unless block.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def migrate(data_source_name, &block)
|
28
|
+
data_sources.fetch(data_source_name).migrate(block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def data_sources
|
32
|
+
@data_sources ||= {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def mapper
|
36
|
+
@mapper ||= Mapper.new
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module YADM
|
2
|
+
module Adapters
|
3
|
+
class << self
|
4
|
+
def fetch(name)
|
5
|
+
registry.fetch(name)
|
6
|
+
rescue KeyError
|
7
|
+
raise NotImplementedError, "Adapter `#{name.inspect}` isn't registered."
|
8
|
+
end
|
9
|
+
|
10
|
+
def register(name, adapter)
|
11
|
+
registry[name] = adapter
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def registry
|
16
|
+
@registry ||= {}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Base
|
21
|
+
def self.included(including_module)
|
22
|
+
including_module.extend ClassMethods
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
def register(name)
|
27
|
+
Adapters.register(name, self)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
|
3
|
+
module YADM
|
4
|
+
module Adapters
|
5
|
+
module CommonSQL
|
6
|
+
attr_reader :connection
|
7
|
+
private :connection
|
8
|
+
|
9
|
+
def get(table_name, id)
|
10
|
+
connection[table_name][id: id]
|
11
|
+
end
|
12
|
+
|
13
|
+
def add(table_name, new_attributes)
|
14
|
+
new_attributes = new_attributes.dup
|
15
|
+
new_attributes.delete(:id)
|
16
|
+
connection[table_name].insert(new_attributes)
|
17
|
+
end
|
18
|
+
|
19
|
+
def change(table_name, id, new_attributes)
|
20
|
+
connection[table_name].where(id: id).update(new_attributes)
|
21
|
+
end
|
22
|
+
|
23
|
+
def remove(table_name, id)
|
24
|
+
connection[table_name].where(id: id).delete
|
25
|
+
end
|
26
|
+
|
27
|
+
def count(table_name)
|
28
|
+
connection[table_name].count
|
29
|
+
end
|
30
|
+
|
31
|
+
def send_query(table_name, query)
|
32
|
+
result = filter(from(table_name), query.criteria.condition, query.arguments)
|
33
|
+
result = order(result, query.criteria.order, query.arguments)
|
34
|
+
result = limit(result, query.criteria.limit, query.arguments)
|
35
|
+
|
36
|
+
result.to_a
|
37
|
+
end
|
38
|
+
|
39
|
+
def from(table_name)
|
40
|
+
connection[table_name]
|
41
|
+
end
|
42
|
+
|
43
|
+
def filter(dataset, condition, arguments)
|
44
|
+
if condition.nil?
|
45
|
+
dataset
|
46
|
+
else
|
47
|
+
sequel_expression = sequelize(condition.expression, arguments)
|
48
|
+
dataset.where(sequel_expression)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def order(dataset, order, arguments)
|
53
|
+
if order.nil?
|
54
|
+
dataset
|
55
|
+
else
|
56
|
+
order.clauses.inject(dataset) do |dataset, clause|
|
57
|
+
sequel_expression = sequelize(clause.expression, arguments)
|
58
|
+
dataset.order_more(sequel_expression.send(clause.type))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def limit(dataset, limit, arguments)
|
64
|
+
if limit.nil? || limit.limit.nil?
|
65
|
+
dataset
|
66
|
+
else
|
67
|
+
take(dataset, limit.limit, arguments)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def migrate(block)
|
72
|
+
block.call(connection)
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
def sequelize(node, arguments)
|
77
|
+
CommonSQL.sequelize(node, arguments)
|
78
|
+
end
|
79
|
+
|
80
|
+
def take(dataset, limit, arguments)
|
81
|
+
number = if limit.is_a?(Criteria::Argument)
|
82
|
+
limit.fetch_from(arguments)
|
83
|
+
else
|
84
|
+
limit
|
85
|
+
end
|
86
|
+
|
87
|
+
dataset.limit(number)
|
88
|
+
end
|
89
|
+
|
90
|
+
class << self
|
91
|
+
def sequelize(node, arguments)
|
92
|
+
case node
|
93
|
+
when Criteria::Expression
|
94
|
+
operator = sequelize_operator(node.method_name)
|
95
|
+
receiver = sequelize(node.receiver, arguments)
|
96
|
+
arguments = node.arguments.map { |arg| sequelize(arg, arguments) }
|
97
|
+
|
98
|
+
Sequel::SQL::ComplexExpression.new(operator, receiver, *arguments)
|
99
|
+
when Criteria::Attribute
|
100
|
+
Sequel::SQL::Identifier.new(node.name)
|
101
|
+
when Criteria::Argument
|
102
|
+
node.fetch_from(arguments)
|
103
|
+
else
|
104
|
+
node
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
def sequelize_operator(operator)
|
110
|
+
case operator
|
111
|
+
when :== then :'='
|
112
|
+
when :& then :AND
|
113
|
+
when :| then :OR
|
114
|
+
else operator
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module YADM
|
2
|
+
module Adapters
|
3
|
+
class Memory
|
4
|
+
include Base
|
5
|
+
|
6
|
+
register :memory
|
7
|
+
|
8
|
+
attr_reader :collections
|
9
|
+
private :collections
|
10
|
+
|
11
|
+
def initialize(connection_params = {})
|
12
|
+
# Memory adapter doesn't need any connection.
|
13
|
+
@collections = Hash.new do |hash, collection_name|
|
14
|
+
hash[collection_name] = Collection.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(collection_name, id)
|
19
|
+
collections[collection_name].get(id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def add(collection_name, record)
|
23
|
+
collections[collection_name].add(record)
|
24
|
+
end
|
25
|
+
|
26
|
+
def change(collection_name, id, new_attributes)
|
27
|
+
collections[collection_name].change(id, new_attributes)
|
28
|
+
end
|
29
|
+
|
30
|
+
def remove(collection_name, id)
|
31
|
+
collections[collection_name].remove(id)
|
32
|
+
end
|
33
|
+
|
34
|
+
def count(collection_name)
|
35
|
+
collections[collection_name].count
|
36
|
+
end
|
37
|
+
|
38
|
+
def send_query(collection_name, query)
|
39
|
+
collections[collection_name].send_query(query)
|
40
|
+
end
|
41
|
+
|
42
|
+
def migrate(block)
|
43
|
+
# do nothing here (memory adapter doesn't need migrations)
|
44
|
+
end
|
45
|
+
|
46
|
+
class Collection
|
47
|
+
attr_reader :records
|
48
|
+
private :records
|
49
|
+
|
50
|
+
def initialize
|
51
|
+
@records = {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def get(id)
|
55
|
+
records.fetch(id)
|
56
|
+
end
|
57
|
+
|
58
|
+
def add(record)
|
59
|
+
next_id.tap do |new_id|
|
60
|
+
records[new_id] = record.merge(id: new_id)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def change(id, new_attributes)
|
65
|
+
records[id].update(new_attributes)
|
66
|
+
end
|
67
|
+
|
68
|
+
def remove(id)
|
69
|
+
records.delete(id)
|
70
|
+
end
|
71
|
+
|
72
|
+
def count
|
73
|
+
records.count
|
74
|
+
end
|
75
|
+
|
76
|
+
def send_query(query)
|
77
|
+
result = filter(all, query.criteria.condition, query.arguments)
|
78
|
+
result = order(result, query.criteria.order, query.arguments)
|
79
|
+
result = limit(result, query.criteria.limit, query.arguments)
|
80
|
+
end
|
81
|
+
|
82
|
+
def all
|
83
|
+
records.values.dup
|
84
|
+
end
|
85
|
+
|
86
|
+
def filter(dataset, condition, arguments)
|
87
|
+
if condition.nil?
|
88
|
+
dataset
|
89
|
+
else
|
90
|
+
dataset.select { |record| matches?(record, condition.expression, arguments) }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def order(dataset, order, arguments)
|
95
|
+
if order.nil?
|
96
|
+
dataset
|
97
|
+
else
|
98
|
+
dataset.sort { |*records| compare(records, order.clauses, arguments) }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def limit(dataset, limit, arguments)
|
103
|
+
if limit.nil? || limit.limit.nil?
|
104
|
+
dataset
|
105
|
+
else
|
106
|
+
take(dataset, limit.limit, arguments)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
def next_id
|
112
|
+
id_sequence.next
|
113
|
+
end
|
114
|
+
|
115
|
+
def id_sequence
|
116
|
+
@sequence ||= Enumerator.new do |yielder|
|
117
|
+
id = 0
|
118
|
+
loop do
|
119
|
+
id += 1
|
120
|
+
yielder.yield id
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def matches?(record, expression, arguments)
|
126
|
+
!!record_eval(record, expression, arguments)
|
127
|
+
end
|
128
|
+
|
129
|
+
def compare(records, clauses, arguments)
|
130
|
+
clauses.inject(0) do |comparison, clause|
|
131
|
+
return comparison unless comparison.zero?
|
132
|
+
|
133
|
+
values = records.map do |record|
|
134
|
+
record_eval(record, clause.expression, arguments)
|
135
|
+
end
|
136
|
+
|
137
|
+
if clause.asc?
|
138
|
+
values.first <=> values.last
|
139
|
+
else
|
140
|
+
values.last <=> values.first
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def take(records, limit, arguments)
|
146
|
+
number = if limit.is_a?(Criteria::Argument)
|
147
|
+
limit.fetch_from(arguments)
|
148
|
+
else
|
149
|
+
limit
|
150
|
+
end
|
151
|
+
|
152
|
+
records.take(number)
|
153
|
+
end
|
154
|
+
|
155
|
+
def record_eval(record, node, arguments)
|
156
|
+
case node
|
157
|
+
when Criteria::Expression
|
158
|
+
receiver = record_eval(record, node.receiver, arguments)
|
159
|
+
arguments = node.arguments.map { |arg| record_eval(record, arg, arguments) }
|
160
|
+
|
161
|
+
receiver.send(node.method_name, *arguments)
|
162
|
+
when Criteria::Attribute
|
163
|
+
record.fetch(node.name) do
|
164
|
+
raise ArgumentError, "#{node.name.inspect} attribute not found."
|
165
|
+
end
|
166
|
+
when Criteria::Argument
|
167
|
+
node.fetch_from(arguments)
|
168
|
+
else
|
169
|
+
node
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'yadm/adapters/common_sql'
|
2
|
+
require 'mysql2'
|
3
|
+
|
4
|
+
module YADM
|
5
|
+
module Adapters
|
6
|
+
class MySQL
|
7
|
+
include Base
|
8
|
+
include CommonSQL
|
9
|
+
|
10
|
+
register :mysql
|
11
|
+
|
12
|
+
def initialize(connection_parameters = {})
|
13
|
+
@connection = Sequel.connect(adapter: :mysql2, **connection_parameters)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'yadm/adapters/common_sql'
|
2
|
+
require 'pg'
|
3
|
+
|
4
|
+
module YADM
|
5
|
+
module Adapters
|
6
|
+
class PostgreSQL
|
7
|
+
include Base
|
8
|
+
include CommonSQL
|
9
|
+
|
10
|
+
register :postgresql
|
11
|
+
|
12
|
+
def initialize(connection_parameters = {})
|
13
|
+
@connection = Sequel.connect(adapter: :postgres, **connection_parameters)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'yadm/adapters/common_sql'
|
2
|
+
require 'sqlite3'
|
3
|
+
|
4
|
+
module YADM
|
5
|
+
module Adapters
|
6
|
+
class Sqlite
|
7
|
+
include Base
|
8
|
+
include CommonSQL
|
9
|
+
|
10
|
+
register :sqlite
|
11
|
+
|
12
|
+
def initialize(connection_parameters = {})
|
13
|
+
@connection = Sequel.connect(adapter: :sqlite, **connection_parameters)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'yadm/criteria/expression'
|
2
|
+
require 'yadm/criteria/attribute'
|
3
|
+
require 'yadm/criteria/argument'
|
4
|
+
require 'yadm/criteria/condition'
|
5
|
+
require 'yadm/criteria/order'
|
6
|
+
require 'yadm/criteria/limit'
|
7
|
+
|
8
|
+
module YADM
|
9
|
+
class Criteria
|
10
|
+
attr_reader :condition, :order, :limit
|
11
|
+
|
12
|
+
def initialize(condition: nil, order: nil, limit: nil)
|
13
|
+
@condition = condition
|
14
|
+
@order = order
|
15
|
+
@limit = limit
|
16
|
+
end
|
17
|
+
|
18
|
+
def merge(other_criteria)
|
19
|
+
self.class.new(
|
20
|
+
condition: Condition.merge(condition, other_criteria.condition),
|
21
|
+
order: Order.merge(order, other_criteria.order),
|
22
|
+
limit: Limit.merge(limit, other_criteria.limit)
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
%i(condition order limit).all? do |method|
|
28
|
+
other.respond_to?(method) && send(method) == other.send(method)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|