hold 1.0.2 → 1.0.3
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 +4 -4
- data/lib/hold.rb +16 -8
- data/lib/hold/error.rb +28 -0
- data/lib/hold/file/hash_repository.rb +49 -42
- data/lib/hold/in_memory.rb +11 -179
- data/lib/hold/in_memory/array_cell.rb +27 -0
- data/lib/hold/in_memory/cell.rb +30 -0
- data/lib/hold/in_memory/hash_repository.rb +28 -0
- data/lib/hold/in_memory/identity_set_repository.rb +49 -0
- data/lib/hold/in_memory/object_cell.rb +30 -0
- data/lib/hold/in_memory/set_repository.rb +28 -0
- data/lib/hold/interfaces.rb +17 -439
- data/lib/hold/interfaces/array_cell.rb +49 -0
- data/lib/hold/interfaces/cell.rb +61 -0
- data/lib/hold/interfaces/hash_repository.rb +63 -0
- data/lib/hold/interfaces/identity_set_repository.rb +118 -0
- data/lib/hold/interfaces/object_cell.rb +103 -0
- data/lib/hold/interfaces/set_repository.rb +37 -0
- data/lib/hold/sequel.rb +12 -11
- data/lib/hold/sequel/identity_set_repository.rb +1 -8
- data/lib/hold/sequel/polymorphic_repository.rb +105 -96
- data/lib/hold/sequel/property_mapper.rb +144 -119
- data/lib/hold/sequel/query_array_cell.rb +19 -14
- data/lib/hold/sequel/repository_observer.rb +25 -21
- data/lib/hold/sequel/with_polymorphic_type_column.rb +145 -99
- data/lib/hold/serialized.rb +2 -104
- data/lib/hold/serialized/hash_repository.rb +50 -0
- data/lib/hold/serialized/identity_set_repository.rb +58 -0
- data/lib/hold/version.rb +2 -1
- metadata +78 -10
@@ -367,16 +367,9 @@ module Hold::Sequel
|
|
367
367
|
end
|
368
368
|
|
369
369
|
def get_by_property(property, value, options={})
|
370
|
-
|
371
|
-
properties_to_fetch[property] = true
|
372
|
-
query(options[:properties]) do |dataset, property_columns|
|
373
|
-
filter = mapper(property).make_filter(value, property_columns[property])
|
374
|
-
dataset.filter(filter)
|
375
|
-
end.single_result
|
370
|
+
get_many_by_property(property, value, options).first
|
376
371
|
end
|
377
372
|
|
378
|
-
|
379
|
-
|
380
373
|
def contains_id?(id)
|
381
374
|
dataset = dataset_to_select_tables(@main_table)
|
382
375
|
id_filter = @identity_mapper.make_filter(id, [@tables_id_columns[@main_table]])
|
@@ -1,121 +1,130 @@
|
|
1
|
-
module Hold
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
1
|
+
module Hold
|
2
|
+
module Sequel
|
3
|
+
# Polymorphic repository
|
4
|
+
class PolymorphicRepository
|
5
|
+
include Hold::IdentitySetRepository
|
6
|
+
|
7
|
+
attr_reader :db, :table, :type_column, :id_column,
|
8
|
+
:type_to_model_class_mapping, :repos_for_model_classes,
|
9
|
+
:model_class_to_type_mapping
|
10
|
+
|
11
|
+
def initialize(db, options = {})
|
12
|
+
@db = db
|
13
|
+
@table = options[:table] || :base
|
14
|
+
@type_column = options[:type_column] || :type
|
15
|
+
@id_column = options[:id_column] || :id
|
16
|
+
@type_to_model_class_mapping = options[:mapping]
|
17
|
+
@model_class_to_type_mapping = @type_to_model_class_mapping.invert
|
18
|
+
|
19
|
+
@repos_for_model_classes = options[:repos] || {}
|
20
|
+
@dataset = @db[@table].select(Sequel.as(@type_column, :_type),
|
21
|
+
Sequel.as(@id_column, :_id))
|
22
|
+
end
|
19
23
|
|
20
|
-
|
21
|
-
|
22
|
-
|
24
|
+
def can_get_class?(model_class)
|
25
|
+
@model_class_to_type_mapping.key?(model_class)
|
26
|
+
end
|
23
27
|
|
24
|
-
|
25
|
-
|
26
|
-
|
28
|
+
def can_set_class?(model_class)
|
29
|
+
@model_class_to_type_mapping.key?(model_class)
|
30
|
+
end
|
27
31
|
|
28
|
-
|
29
|
-
|
30
|
-
|
32
|
+
def get_repo_dependencies_from(repo_set)
|
33
|
+
@type_to_model_class_mapping.each do |_, model_class|
|
34
|
+
@repos_for_model_classes[model_class] ||=
|
35
|
+
repo_set.repo_for_model_class(model_class)
|
36
|
+
end
|
31
37
|
end
|
32
|
-
end
|
33
38
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
def type_to_repo_mapping
|
40
|
+
@type_to_repo_mapping ||=
|
41
|
+
begin
|
42
|
+
@type_to_repo_mapping.each_with_obeject({}) do |(t, m), hash|
|
43
|
+
hash[t] = @repos_for_model_classes[m]
|
44
|
+
end
|
45
|
+
end
|
39
46
|
end
|
40
|
-
end
|
41
47
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
48
|
+
def construct_entity(property_hash, _row = nil)
|
49
|
+
type = property_hash[:_type] || (fail 'missing _type in result row')
|
50
|
+
@type_to_model_class_mapping[type].new(property_hash)
|
51
|
+
end
|
46
52
|
|
47
|
-
|
48
|
-
|
49
|
-
|
53
|
+
def transaction(*p, &b)
|
54
|
+
@db.transaction(*p, &b)
|
55
|
+
end
|
50
56
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
57
|
+
# - Takes multiple result rows with type and id column
|
58
|
+
# - Groups the IDs by type and does a separate get_many_by_ids query on
|
59
|
+
# the relevant repo
|
60
|
+
# - Combines the results from the separate queries putting them into the
|
61
|
+
# order of the IDs from the original rows (or in the order of the ids
|
62
|
+
# given, where they are given)
|
63
|
+
def load_from_rows(rows, options = {}, ids = nil)
|
64
|
+
ids ||= rows.map { |row| row[:_id] }
|
65
|
+
ids_by_type = Hash.new { |h, k| h[k] = [] }
|
66
|
+
rows.each { |row| ids_by_type[row[:_type]] << row[:_id] }
|
67
|
+
results_by_id = {}
|
68
|
+
ids_by_type.each do |type, type_ids|
|
69
|
+
repo = type_to_repo_mapping[type] ||
|
70
|
+
(fail "PolymorphicRepository: no repo found for type #{type}")
|
71
|
+
repo.get_many_by_ids(type_ids, options)
|
72
|
+
.each_with_index { |res, i| results_by_id[type_ids[i]] = res }
|
64
73
|
end
|
74
|
+
results_by_id.values_at(*ids)
|
65
75
|
end
|
66
|
-
results_by_id.values_at(*ids)
|
67
|
-
end
|
68
76
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
def get_with_dataset(options={}, &b)
|
75
|
-
dataset = @dataset
|
76
|
-
dataset = yield @dataset if block_given?
|
77
|
-
row = dataset.limit(1).first and load_from_row(row, options)
|
78
|
-
end
|
77
|
+
def load_from_row(row, options = {})
|
78
|
+
repo =
|
79
|
+
type_to_repo_mapping[row[:_type]] ||
|
80
|
+
(fail "PolymorphicRepository: no repo found for type #{row[:_type]}")
|
79
81
|
|
80
|
-
|
81
|
-
|
82
|
-
end
|
83
|
-
|
84
|
-
def get_many_by_ids(ids, options={})
|
85
|
-
rows = @dataset.filter(@id_column => ids).all
|
86
|
-
load_from_rows(rows, options, ids)
|
87
|
-
end
|
82
|
+
repo.get_by_id(row[:_id], options)
|
83
|
+
end
|
88
84
|
|
89
|
-
|
90
|
-
|
91
|
-
|
85
|
+
def get_with_dataset(options = {})
|
86
|
+
dataset = @dataset
|
87
|
+
dataset = yield @dataset if block_given?
|
88
|
+
(row = dataset.limit(1).first) && load_from_row(row, options)
|
89
|
+
end
|
92
90
|
|
91
|
+
def get_by_id(id, options = {})
|
92
|
+
get_with_dataset(options) { |ds| ds.filter(@id_column => id) }
|
93
|
+
end
|
93
94
|
|
95
|
+
def get_many_by_ids(ids, options = {})
|
96
|
+
rows = @dataset.filter(@id_column => ids).all
|
97
|
+
load_from_rows(rows, options, ids)
|
98
|
+
end
|
94
99
|
|
100
|
+
def contains_id?(id)
|
101
|
+
!@dataset.filter(@id_column => id).select(1).limit(1).single_value.nil?
|
102
|
+
end
|
95
103
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
104
|
+
def store(object)
|
105
|
+
repo = @repos_for_model_classes[object.class] || (fail StdError)
|
106
|
+
repo.store(id, object)
|
107
|
+
end
|
100
108
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
109
|
+
def store_new(object)
|
110
|
+
repo = @repos_for_model_classes[object.class] || (fail StdError)
|
111
|
+
repo.store_new(id, object)
|
112
|
+
end
|
105
113
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
114
|
+
def update(entity, update_entity)
|
115
|
+
repo = @repos_for_model_classes[entity.class] || (fail StdError)
|
116
|
+
repo.update(entity, update_entity)
|
117
|
+
end
|
110
118
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
119
|
+
def update_by_id(id, update_entity)
|
120
|
+
repo = @repos_for_model_classes[update_entity.class] || (fail StdError)
|
121
|
+
repo.update_by_id(id, update_entity)
|
122
|
+
end
|
115
123
|
|
116
|
-
|
117
|
-
|
118
|
-
|
124
|
+
def delete(object)
|
125
|
+
repo = @repos_for_model_classes[object.class] || (fail StdError)
|
126
|
+
repo.delete(object)
|
127
|
+
end
|
119
128
|
end
|
120
129
|
end
|
121
130
|
end
|
@@ -1,138 +1,163 @@
|
|
1
|
-
module Hold
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
module Hold
|
2
|
+
module Sequel
|
3
|
+
# Abstract superclass.
|
4
|
+
#
|
5
|
+
# Responsibility of a PropertyMapper is to map data for a particular
|
6
|
+
# property of a model class, between the instances of that model class, and
|
7
|
+
# the database
|
8
|
+
class PropertyMapper
|
9
|
+
def self.setter_dependencies_for(_options = {})
|
10
|
+
{}
|
11
|
+
end
|
7
12
|
|
8
|
-
|
13
|
+
attr_reader :repository, :property_name, :property
|
9
14
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
# If you pass a block, it will be instance_evalled, allowing you to create
|
16
|
+
# one-off custom property mappers by overriding bits of this
|
17
|
+
# implementation in the block.
|
18
|
+
def initialize(repo, property_name, _options = nil, &block)
|
19
|
+
@repository = repo
|
20
|
+
@property_name = property_name
|
21
|
+
instance_eval(&block) if block
|
22
|
+
end
|
17
23
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
# columns: column names to include in a SELECT in order to select this
|
25
|
+
# property. These should be qualified with the relevant table name but not
|
26
|
+
# aliased.
|
27
|
+
#
|
28
|
+
# aliases: the above columns, aliased for use in the SELECT clause. be
|
29
|
+
# alias should something unique which the mapper can later use to retreive
|
30
|
+
# from a result row.
|
31
|
+
#
|
32
|
+
# Any tables which need to be present in the FROM clause in order to
|
33
|
+
# select the columns. Relevant joins will be constructed by the parent
|
34
|
+
# repo.
|
35
|
+
#
|
36
|
+
# A 'preferred_table' hint may be passed by the repo to indicate that it'd
|
37
|
+
# prefer you load the column off a particular table; at present this is
|
38
|
+
# only used by the IdentityMapper
|
39
|
+
def columns_aliases_and_tables_for_select(_preferred_table = nil)
|
40
|
+
[[], [], []]
|
41
|
+
end
|
32
42
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
# Obtains the value of this property from a sequel result row and/or
|
44
|
+
# identity value.
|
45
|
+
#
|
46
|
+
# where the mapper has columns_aliases_and_tables_for_select, it will get
|
47
|
+
# passed a result row object here which contains the sql values for these
|
48
|
+
# columns (amongst others potentially)
|
49
|
+
#
|
50
|
+
# Where the identity value is available it will also be passed.
|
51
|
+
#
|
52
|
+
# One or other of id, row must always be passed.
|
53
|
+
def load_value(_row = nil, _id = nil, _properties = nil)
|
54
|
+
end
|
43
55
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
56
|
+
# called inside the INSERT transaction for insertion of the given entity.
|
57
|
+
#
|
58
|
+
# this is called first thing before insert rows are built (via
|
59
|
+
# build_insert_row) for each table of the repo.
|
60
|
+
def pre_insert(_entity)
|
61
|
+
end
|
50
62
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
63
|
+
# called inside the UPDATE transaction for insertion of the given entity.
|
64
|
+
#
|
65
|
+
# this is called first thing before update rows are built (via
|
66
|
+
# build_update_row) for each table of the repo.
|
67
|
+
#
|
68
|
+
# anything returned from pre_update will be passed to post_update's
|
69
|
+
# data_from_pre_update arg if the update succeeds.
|
70
|
+
def pre_update(_entity, _update_entity)
|
71
|
+
end
|
60
72
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
73
|
+
# called inside the DELETE transaction for a given entity.
|
74
|
+
#
|
75
|
+
# this is called first thing before rows are deleted for each table of the
|
76
|
+
# repo.
|
77
|
+
def pre_delete(_entity)
|
78
|
+
end
|
66
79
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
80
|
+
# called inside the DELETE transaction for a given entity.
|
81
|
+
#
|
82
|
+
# this is called last thing after rows are deleted for each table of the
|
83
|
+
# repo.
|
84
|
+
def post_delete(_entity)
|
85
|
+
end
|
72
86
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
87
|
+
# gets this property off the entity, and sets associated keys on a sequel
|
88
|
+
# row hash for insertion into the given table. May be passed an ID if an
|
89
|
+
# last_insert_id id value for the entity was previously obtained from an
|
90
|
+
# ID sequence on insertion into another table as part of the same combined
|
91
|
+
# entity store_new.
|
92
|
+
#
|
93
|
+
# this is called inside the transaction which wraps the insert, so this is
|
94
|
+
# effectively your pre-insert hook and you can safely do other things
|
95
|
+
# inside it in the knowledge they'll be rolled back in the event of a
|
96
|
+
# subsequent problem.
|
97
|
+
def build_insert_row(_entity, _table, _row, _id = nil)
|
98
|
+
end
|
83
99
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
100
|
+
# gets this property off the update_entity, and sets associated keys on a
|
101
|
+
# sequel row hash for update of the given table for the given entity.
|
102
|
+
#
|
103
|
+
# as with build_update_row, this is done inside the update transaction,
|
104
|
+
# it's effectively your pre-update hook.
|
105
|
+
def build_update_row(_update_entity, _table, _row)
|
106
|
+
end
|
91
107
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
108
|
+
# used to make a sequel filter condition setting relevant columns equal to
|
109
|
+
# values equivalent to the given property value. May raise if mapper
|
110
|
+
# doesn't support this
|
111
|
+
def make_filter(_value, _columns_mapped_to)
|
112
|
+
fail Hold::UnsupportedOperation
|
113
|
+
end
|
97
114
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
115
|
+
# As for make_filter but takes multiple possible values and does a column
|
116
|
+
# IN (1,2,3,4) type thing.
|
117
|
+
def make_multi_filter(_values, _columns_mapped_to)
|
118
|
+
fail Hold::UnsupportedOperation
|
119
|
+
end
|
102
120
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
rows
|
109
|
-
|
110
|
-
|
121
|
+
# like load_value, but works in a batched fashion, allowing a batched
|
122
|
+
# loading strategy to be used for associated objects.
|
123
|
+
# takes a block and yields the loaded values one at a time to it together
|
124
|
+
# with their index
|
125
|
+
def load_values(rows = nil, ids = nil, properties = nil)
|
126
|
+
if rows
|
127
|
+
rows.each_with_index do |row, i|
|
128
|
+
yield load_value(row, ids && ids[i], properties), i
|
129
|
+
end
|
130
|
+
else
|
131
|
+
ids.each_with_index do |id, i|
|
132
|
+
yield load_value(nil, id, properties), i
|
133
|
+
end
|
134
|
+
end
|
111
135
|
end
|
112
|
-
end
|
113
136
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
137
|
+
# called after rows built via build_insert_row have successfully been used
|
138
|
+
# in a INSERT for the entity passed. Should update the entity property,
|
139
|
+
# where appropriate, with any default values which were supplied by the
|
140
|
+
# repository (via default_for) on insert, and should do any additional
|
141
|
+
# work in order to save any values which are not mapped to columns on one
|
142
|
+
# of the repo's own :tables
|
143
|
+
#
|
144
|
+
# Is also passed the last_insert_id resulting from any insert, to help
|
145
|
+
# fill out any autoincrement primary key column.
|
146
|
+
#
|
147
|
+
# is executed inside the same transaction as the INSERT
|
148
|
+
def post_insert(_entity, _rows, _last_insert_id = nil)
|
149
|
+
end
|
126
150
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
151
|
+
# called after rows built via build_update_row have successfully been used
|
152
|
+
# in a UPDATE for the id and update_entity passed. Should update the
|
153
|
+
# entity property, where appropriate, with any default values which were
|
154
|
+
# supplied by the repository (via default_for) on update, and should do
|
155
|
+
# any additional work in order to save any values which are not mapped to
|
156
|
+
# columns on one of the repo's own :tables
|
157
|
+
#
|
158
|
+
# is executed inside the same transaction as the UPDATE
|
159
|
+
def post_update(_entity, _update_entity, _rows, _data_from_pre_update)
|
160
|
+
end
|
135
161
|
end
|
136
|
-
|
137
162
|
end
|
138
163
|
end
|