praxis-mapper 3.1.1
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/.gitignore +26 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +83 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +102 -0
- data/Guardfile +11 -0
- data/LICENSE +22 -0
- data/README.md +19 -0
- data/Rakefile +14 -0
- data/lib/praxis-mapper/config_hash.rb +40 -0
- data/lib/praxis-mapper/connection_manager.rb +102 -0
- data/lib/praxis-mapper/finalizable.rb +38 -0
- data/lib/praxis-mapper/identity_map.rb +532 -0
- data/lib/praxis-mapper/logging.rb +22 -0
- data/lib/praxis-mapper/model.rb +430 -0
- data/lib/praxis-mapper/query/base.rb +213 -0
- data/lib/praxis-mapper/query/sql.rb +183 -0
- data/lib/praxis-mapper/query_statistics.rb +46 -0
- data/lib/praxis-mapper/resource.rb +226 -0
- data/lib/praxis-mapper/support/factory_girl.rb +104 -0
- data/lib/praxis-mapper/support/memory_query.rb +34 -0
- data/lib/praxis-mapper/support/memory_repository.rb +44 -0
- data/lib/praxis-mapper/support/schema_dumper.rb +66 -0
- data/lib/praxis-mapper/support/schema_loader.rb +56 -0
- data/lib/praxis-mapper/support.rb +2 -0
- data/lib/praxis-mapper/version.rb +5 -0
- data/lib/praxis-mapper.rb +60 -0
- data/praxis-mapper.gemspec +38 -0
- data/spec/praxis-mapper/connection_manager_spec.rb +117 -0
- data/spec/praxis-mapper/identity_map_spec.rb +905 -0
- data/spec/praxis-mapper/logging_spec.rb +9 -0
- data/spec/praxis-mapper/memory_repository_spec.rb +56 -0
- data/spec/praxis-mapper/model_spec.rb +389 -0
- data/spec/praxis-mapper/query/base_spec.rb +317 -0
- data/spec/praxis-mapper/query/sql_spec.rb +184 -0
- data/spec/praxis-mapper/resource_spec.rb +154 -0
- data/spec/praxis_mapper_spec.rb +21 -0
- data/spec/spec_fixtures.rb +12 -0
- data/spec/spec_helper.rb +63 -0
- data/spec/support/spec_models.rb +215 -0
- data/spec/support/spec_resources.rb +39 -0
- metadata +298 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
module Praxis::Mapper
|
4
|
+
module Query
|
5
|
+
|
6
|
+
# An SQL 'SELECT' statement assembler.
|
7
|
+
# Assumes ISO SQL:2008 unless otherwise noted.
|
8
|
+
# TODO: rename to MySql or MySql5 or MySql51 or something
|
9
|
+
#
|
10
|
+
# The SQL SELECT statement returns a result set of records from one or more tables.
|
11
|
+
#
|
12
|
+
# The SELECT statement has two mandatory clauses:
|
13
|
+
# - SELECT specifies which columns/aliases to return.
|
14
|
+
# - FROM specifies which tables/views to query.
|
15
|
+
#
|
16
|
+
# The SELECT statement has many optional clauses:
|
17
|
+
# - WHERE specifies which rows to retrieve.
|
18
|
+
# - GROUP BY groups rows sharing a property so that an aggregate function can be applied to each group.
|
19
|
+
# - HAVING selects among the groups defined by the GROUP BY clause.
|
20
|
+
# - ORDER BY specifies an order in which to return the rows.
|
21
|
+
# - LIMIT specifies how many rows to return (non-standard).
|
22
|
+
#
|
23
|
+
# Currently only SELECT, FROM, WHERE and LIMIT has been implemented.
|
24
|
+
#
|
25
|
+
# @example "SELECT column1, column2 FROM table1 WHERE column1=value1 AND column2=value2"
|
26
|
+
#
|
27
|
+
# @see http://en.wikipedia.org/wiki/Select_(SQL)
|
28
|
+
class Sql < Base
|
29
|
+
|
30
|
+
# Executes a 'SELECT' statement.
|
31
|
+
#
|
32
|
+
# @param identity [Symbol|Array] a simple or composite key for this model
|
33
|
+
# @param values [Array] list of identifier values (ideally a sorted set)
|
34
|
+
# @return [Array] SQL result set
|
35
|
+
#
|
36
|
+
# @example numeric key
|
37
|
+
# _multi_get(:id, [1, 2])
|
38
|
+
# @example string key
|
39
|
+
# _multi_get(:uid, ['foo', 'bar'])
|
40
|
+
# @example composite key (possibly a combination of numeric and string keys)
|
41
|
+
# _multi_get([:cloud_id, :account_id], [['foo1', 'bar1'], ['foo2', 'bar2']])
|
42
|
+
def _multi_get(identity, values)
|
43
|
+
dataset = connection[model.table_name.to_sym].where(identity => values)
|
44
|
+
|
45
|
+
# MySQL 5.1 won't use an index for a multi-column IN clause. Consequently, when adding
|
46
|
+
# multi-column IN clauses, we also add a single-column IN clause for the first column of
|
47
|
+
# the multi-column IN-clause. In this way, MySQL will be able to use an index for the
|
48
|
+
# single-column IN clause but will use the multi-column IN clauses to limit which
|
49
|
+
# records are returned.
|
50
|
+
if identity.kind_of?(Array)
|
51
|
+
dataset = dataset.where(identity.first => values.collect(&:first))
|
52
|
+
end
|
53
|
+
|
54
|
+
# preserve existing where condition from query
|
55
|
+
if @where
|
56
|
+
dataset = dataset.where(@where)
|
57
|
+
end
|
58
|
+
|
59
|
+
clause = dataset.opts[:where].sql_literal(dataset)
|
60
|
+
|
61
|
+
original_where = @where
|
62
|
+
|
63
|
+
self.where clause
|
64
|
+
_execute
|
65
|
+
ensure
|
66
|
+
@where = original_where
|
67
|
+
end
|
68
|
+
|
69
|
+
# Executes this SQL statement.
|
70
|
+
# Does not perform any validation of the statement before execution.
|
71
|
+
#
|
72
|
+
# @return [Array] result-set
|
73
|
+
def _execute
|
74
|
+
Praxis::Mapper.logger.debug "SQL:\n#{self.describe}\n"
|
75
|
+
self.statistics[:datastore_interactions] += 1
|
76
|
+
start_time = Time.now
|
77
|
+
|
78
|
+
rows = connection.fetch(self.sql).to_a
|
79
|
+
|
80
|
+
self.statistics[:datastore_interaction_time] += (Time.now - start_time)
|
81
|
+
return rows
|
82
|
+
end
|
83
|
+
|
84
|
+
# @see #sql
|
85
|
+
def describe
|
86
|
+
self.sql
|
87
|
+
end
|
88
|
+
|
89
|
+
# Constructs a raw SQL statement.
|
90
|
+
# No validation is performed here (security risk?).
|
91
|
+
#
|
92
|
+
# @param sql_text a custom SQL query
|
93
|
+
#
|
94
|
+
def raw(sql_text)
|
95
|
+
@raw_query = sql_text
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [String] raw or assembled SQL statement
|
99
|
+
def sql
|
100
|
+
if @raw_query
|
101
|
+
@raw_query
|
102
|
+
else
|
103
|
+
[select_clause, from_clause, where_clause, limit_clause].compact.join("\n")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# @return [String] SQL 'SELECT' clause
|
108
|
+
def select_clause
|
109
|
+
columns = []
|
110
|
+
if select
|
111
|
+
select.each do |alias_name, column_name|
|
112
|
+
if column_name
|
113
|
+
# alias_name is always a String, not a Symbol
|
114
|
+
columns << "#{column_name} AS #{alias_name}"
|
115
|
+
else
|
116
|
+
columns << (alias_name.is_a?(Symbol) ? alias_name.to_s : alias_name)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
else
|
120
|
+
columns << '*'
|
121
|
+
end
|
122
|
+
|
123
|
+
"SELECT #{columns.join(', ')}"
|
124
|
+
end
|
125
|
+
|
126
|
+
# @return [String] SQL 'FROM' clause
|
127
|
+
#
|
128
|
+
# FIXME: use ANSI SQL double quotes instead of MySQL backticks
|
129
|
+
# @see http://stackoverflow.com/questions/261455/using-backticks-around-field-names
|
130
|
+
def from_clause
|
131
|
+
"FROM `#{model.table_name}`"
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return [String] SQL 'LIMIT' clause or nil
|
135
|
+
#
|
136
|
+
# NOTE: implementation-dependent; not part of ANSI SQL
|
137
|
+
# TODO: replace with ISO SQL:2008 FETCH FIRST clause
|
138
|
+
def limit_clause
|
139
|
+
if self.limit
|
140
|
+
return "LIMIT #{self.limit}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Constructs the 'WHERE' clause with all active scopes (read: named conditions).
|
145
|
+
#
|
146
|
+
# @return [String] an SQL 'WHERE' clause or nil if no conditions
|
147
|
+
#
|
148
|
+
# FIXME: use ANSI SQL double quotes instead of MySQL backticks
|
149
|
+
# FIXME: Doesn't sanitize any values. Could be "fun" later (where fun means a horrible security hole)
|
150
|
+
# TODO: add per-model scopes, ie, servers might have a scope for type = "GenericServer"
|
151
|
+
def where_clause
|
152
|
+
# collects and compacts conditions as defined in identity map and model
|
153
|
+
conditions = identity_map.scope.collect do |name, condition|
|
154
|
+
# checks if this condition has been banned for this model
|
155
|
+
unless model.excluded_scopes.include? name
|
156
|
+
column, value = condition # example: "user_id", 123
|
157
|
+
case value
|
158
|
+
when Integer
|
159
|
+
"`#{column}`=#{value}"
|
160
|
+
when String
|
161
|
+
"`#{column}`='#{value}'"
|
162
|
+
when NilClass
|
163
|
+
"`#{column}` IS NULL"
|
164
|
+
else
|
165
|
+
raise "unknown type for scope #{name} with condition #{condition}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end.compact
|
169
|
+
|
170
|
+
conditions << where if where
|
171
|
+
|
172
|
+
if conditions.any?
|
173
|
+
return "WHERE #{conditions.join(" AND ")}"
|
174
|
+
else
|
175
|
+
nil
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Praxis::Mapper
|
2
|
+
|
3
|
+
class QueryStatistics
|
4
|
+
|
5
|
+
def initialize(queries_by_model)
|
6
|
+
@queries_by_model = queries_by_model
|
7
|
+
end
|
8
|
+
|
9
|
+
# sums up statistics across all queries, indexed by model
|
10
|
+
def sum_totals_by_model
|
11
|
+
@sum_totals_by_model ||= begin
|
12
|
+
totals = Hash.new { |hash, key| hash[key] = Hash.new(0) }
|
13
|
+
|
14
|
+
@queries_by_model.each do |model, queries|
|
15
|
+
totals[model][:query_count] = queries.length
|
16
|
+
queries.each do |query|
|
17
|
+
query.statistics.each do |stat, value|
|
18
|
+
totals[model][stat] += value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
totals[model][:datastore_interaction_time] = totals[model][:datastore_interaction_time]
|
23
|
+
end
|
24
|
+
|
25
|
+
totals
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# sums up statistics across all models and queries
|
30
|
+
def sum_totals
|
31
|
+
@sum_totals ||= begin
|
32
|
+
totals = Hash.new(0)
|
33
|
+
|
34
|
+
sum_totals_by_model.each do |_, model_totals|
|
35
|
+
model_totals.each do |stat, value|
|
36
|
+
totals[stat] += value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
totals
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
# A resource creates a data store and instantiates a list of models that it wishes to load, building up the overall set of data that it will need.
|
4
|
+
# Once that is complete, the data set is iterated and a resultant view is generated.
|
5
|
+
module Praxis::Mapper
|
6
|
+
class Resource
|
7
|
+
extend Finalizable
|
8
|
+
|
9
|
+
attr_accessor :record
|
10
|
+
|
11
|
+
# TODO: also support an attribute of sorts on the versioned resource module. ie, V1::Resources.api_version.
|
12
|
+
# replacing the self == Praxis::Mapper::Resource condition below.
|
13
|
+
def self.inherited(klass)
|
14
|
+
super
|
15
|
+
|
16
|
+
# It is expected that each versioned set of resources will have a common Base class.
|
17
|
+
# self is Praxis::Mapper::Resource only for Base resource classes which are versioned.
|
18
|
+
if self == Praxis::Mapper::Resource
|
19
|
+
klass.instance_variable_set(:@model_map, Hash.new)
|
20
|
+
elsif defined?(@model_map)
|
21
|
+
klass.instance_variable_set(:@model_map, @model_map)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.model_map
|
26
|
+
if defined? @model_map
|
27
|
+
return @model_map
|
28
|
+
else
|
29
|
+
return {}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
#TODO: Take symbol/string and resolve the klass (but lazily, so we don't care about load order)
|
35
|
+
def self.model(klass=nil)
|
36
|
+
if klass
|
37
|
+
@model = klass
|
38
|
+
self.model_map[klass] = self
|
39
|
+
else
|
40
|
+
@model
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self._finalize!
|
45
|
+
finalize_resource_delegates
|
46
|
+
define_model_accessors
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.finalize_resource_delegates
|
51
|
+
return unless @resource_delegates
|
52
|
+
|
53
|
+
@resource_delegates.each do |record_name, record_attributes|
|
54
|
+
record_attributes.each do |record_attribute|
|
55
|
+
self.define_resource_delegate(record_name, record_attribute)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def self.define_model_accessors
|
62
|
+
return if model.nil?
|
63
|
+
model.associations.each do |k,v|
|
64
|
+
if self.instance_methods.include? k
|
65
|
+
warn "WARNING: #{self.name} already has method named #{k.inspect}. Will not define accessor for resource association."
|
66
|
+
end
|
67
|
+
define_model_association_accessor(k,v)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def self.for_record(record)
|
73
|
+
return record._resource if record._resource
|
74
|
+
|
75
|
+
if resource_class_for_record = model_map[record.class]
|
76
|
+
return record._resource = resource_class_for_record.new(record)
|
77
|
+
else
|
78
|
+
version = self.name.split("::")[0..-2].join("::")
|
79
|
+
resource_name = record.class.name.split("::").last
|
80
|
+
|
81
|
+
raise "No resource class corresponding to the model class '#{record.class}' is defined. (Did you forget to define '#{version}::#{resource_name}'?)"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
def self.wrap(records)
|
87
|
+
case records
|
88
|
+
when Model
|
89
|
+
return self.for_record(records)
|
90
|
+
when nil
|
91
|
+
# Return an empty set if `records` is nil
|
92
|
+
return []
|
93
|
+
else
|
94
|
+
return records.collect { |record| self.for_record(record) }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
def self.get(condition)
|
100
|
+
record = self.model.get(condition)
|
101
|
+
|
102
|
+
self.wrap(record)
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.all(condition={})
|
106
|
+
records = self.model.all(condition)
|
107
|
+
|
108
|
+
self.wrap(records)
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
def initialize(record)
|
113
|
+
@record = record
|
114
|
+
end
|
115
|
+
|
116
|
+
def respond_to_missing?(name,*)
|
117
|
+
@record.respond_to?(name) || super
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.resource_delegates
|
121
|
+
@resource_delegates ||= {}
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.resource_delegate(spec)
|
125
|
+
spec.each do |resource_name, attributes|
|
126
|
+
resource_delegates[resource_name] = attributes
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Defines wrapers for model associations that return Resources
|
131
|
+
def self.define_model_association_accessor(name, association_spec)
|
132
|
+
association_model = association_spec.fetch(:model)
|
133
|
+
association_resource_class = model_map[association_model]
|
134
|
+
if association_resource_class
|
135
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
136
|
+
def #{name}
|
137
|
+
records = record.#{name}
|
138
|
+
return nil if records.nil?
|
139
|
+
@__#{name} ||= #{association_resource_class}.wrap(records)
|
140
|
+
end
|
141
|
+
RUBY
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.define_resource_delegate(resource_name, resource_attribute)
|
146
|
+
related_model = model.associations[resource_name][:model]
|
147
|
+
related_association = related_model.associations[resource_attribute]
|
148
|
+
|
149
|
+
if related_association
|
150
|
+
self.define_delegation_for_related_association(resource_name, resource_attribute, related_association)
|
151
|
+
else
|
152
|
+
self.define_delegation_for_related_attribute(resource_name, resource_attribute)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
|
158
|
+
def self.define_delegation_for_related_attribute(resource_name, resource_attribute)
|
159
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
160
|
+
def #{resource_attribute}
|
161
|
+
@__#{resource_attribute} ||= if (rec = self.#{resource_name})
|
162
|
+
rec.#{resource_attribute}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
RUBY
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.define_delegation_for_related_association(resource_name, resource_attribute, related_association)
|
169
|
+
related_resource_class = model_map[related_association[:model]]
|
170
|
+
return unless related_resource_class
|
171
|
+
|
172
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
173
|
+
def #{resource_attribute}
|
174
|
+
@__#{resource_attribute} ||= if (rec = self.#{resource_name})
|
175
|
+
if (related = rec.#{resource_attribute})
|
176
|
+
#{related_resource_class.name}.wrap(related)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
RUBY
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.define_accessor(name)
|
184
|
+
if name.to_s =~ /\?/
|
185
|
+
ivar_name = "is_#{name.to_s[0..-2]}"
|
186
|
+
else
|
187
|
+
ivar_name = "#{name}"
|
188
|
+
end
|
189
|
+
|
190
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
191
|
+
def #{name}
|
192
|
+
return @__#{ivar_name} if defined? @__#{ivar_name}
|
193
|
+
@__#{ivar_name} = record.#{name}
|
194
|
+
end
|
195
|
+
RUBY
|
196
|
+
end
|
197
|
+
|
198
|
+
def method_missing(name,*args)
|
199
|
+
if @record.respond_to?(name)
|
200
|
+
self.class.define_accessor(name)
|
201
|
+
self.send(name)
|
202
|
+
else
|
203
|
+
super
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def self.member_name
|
208
|
+
@_member_name ||= self.name.split("::").last.underscore
|
209
|
+
end
|
210
|
+
|
211
|
+
def self.collection_name
|
212
|
+
@_collection_name ||= self.member_name.pluralize
|
213
|
+
end
|
214
|
+
|
215
|
+
def member_name
|
216
|
+
self.class.member_name
|
217
|
+
end
|
218
|
+
|
219
|
+
alias :type :member_name
|
220
|
+
|
221
|
+
def collection_name
|
222
|
+
self.class.collection_name
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# Hackish write support for Praxis::Mapper, needed for FactoryGirl.create calls.
|
2
|
+
# TODO: get rid of this and use current FactoryGirl features to do it the right way.
|
3
|
+
|
4
|
+
module Praxis::Mapper
|
5
|
+
class Model
|
6
|
+
|
7
|
+
def _data
|
8
|
+
@data
|
9
|
+
end
|
10
|
+
|
11
|
+
def save!
|
12
|
+
@new_record = true
|
13
|
+
unless Praxis::Mapper::IdentityMap.current.add_records([self]).include? self
|
14
|
+
raise "Conflict trying to save record with type: #{self.class} and data:\n#{@data.pretty_inspect}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method :original_method_missing, :method_missing
|
19
|
+
|
20
|
+
def method_missing(name, *args)
|
21
|
+
if name.to_s =~ /=$/
|
22
|
+
name = name.to_s.sub!("=", "").to_sym
|
23
|
+
value = args.first
|
24
|
+
|
25
|
+
if self.class.associations.has_key?(name)
|
26
|
+
set_association(name, value)
|
27
|
+
elsif self.class.serialized_fields.has_key?(name)
|
28
|
+
set_serialized_field(name, value)
|
29
|
+
else
|
30
|
+
if value.kind_of?(Praxis::Mapper::Model)
|
31
|
+
raise "Can not set #{self.class.name}##{name} with Model instance. Are you missing an association?"
|
32
|
+
end
|
33
|
+
@data[name] = value
|
34
|
+
end
|
35
|
+
else
|
36
|
+
original_method_missing(name, *args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def set_serialized_field(name,value)
|
42
|
+
@deserialized_data[name] = value
|
43
|
+
|
44
|
+
case self.class.serialized_fields[name]
|
45
|
+
when :json
|
46
|
+
@data[name] = JSON.dump(value)
|
47
|
+
when :yaml
|
48
|
+
@data[name] = YAML.dump(value)
|
49
|
+
else
|
50
|
+
@data[name] = value # dunno
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def set_association(name, value)
|
55
|
+
spec = self.class.associations.fetch(name)
|
56
|
+
|
57
|
+
case spec[:type]
|
58
|
+
when :one_to_many
|
59
|
+
raise "can not set one_to_many associations to nil" if value.nil?
|
60
|
+
primary_key = @data[spec[:primary_key]]
|
61
|
+
setter_name = "#{spec[:key]}="
|
62
|
+
Array(value).each { |item| item.send(setter_name, primary_key) }
|
63
|
+
when :many_to_one
|
64
|
+
primary_key = value && value.send(spec[:primary_key])
|
65
|
+
@data[spec[:key]] = primary_key
|
66
|
+
else
|
67
|
+
raise "can not handle associations of type #{spec[:type]}"
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def initialize(data={})
|
74
|
+
@data = data
|
75
|
+
@deserialized_data = {}
|
76
|
+
@new_record = false
|
77
|
+
end
|
78
|
+
|
79
|
+
attr_accessor :new_record
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
class IdentityMap
|
84
|
+
|
85
|
+
def persist!
|
86
|
+
@rows.each_with_object(Hash.new) do |(model, records), inserted|
|
87
|
+
next unless (table = model.table_name)
|
88
|
+
|
89
|
+
db ||= self.connection(model.repository_name)
|
90
|
+
|
91
|
+
new_records = records.select(&:new_record)
|
92
|
+
next if new_records.empty?
|
93
|
+
|
94
|
+
db[table.to_sym].multi_insert new_records.collect(&:_data)
|
95
|
+
|
96
|
+
new_records.each { |rec| rec.new_record = false }
|
97
|
+
inserted[model] = new_records
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# In-memory query designed for use with the MemoryRepository for specs
|
2
|
+
|
3
|
+
module Praxis::Mapper
|
4
|
+
module Support
|
5
|
+
class MemoryQuery < Praxis::Mapper::Query::Base
|
6
|
+
|
7
|
+
def collection
|
8
|
+
connection.collection(model.table_name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def _multi_get(key, values)
|
12
|
+
results = values.collect do |value|
|
13
|
+
connection.all(model, key => value)
|
14
|
+
end.flatten.uniq
|
15
|
+
|
16
|
+
results.select do |result|
|
17
|
+
where.nil? || where.all? do |k,v|
|
18
|
+
result[k] == v
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def _execute
|
24
|
+
connection.all(model.table_name, self.where||{}).to_a
|
25
|
+
end
|
26
|
+
|
27
|
+
# Subclasses Must Implement
|
28
|
+
def describe
|
29
|
+
raise "subclass responsibility"
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Unoptimized, highly inefficient in-memory datastore designed for use with specs.
|
2
|
+
|
3
|
+
module Praxis::Mapper
|
4
|
+
module Support
|
5
|
+
class MemoryRepository
|
6
|
+
|
7
|
+
attr_reader :collections
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
clear!
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear!
|
14
|
+
@collections = Hash.new do |hash, collection_name|
|
15
|
+
hash[collection_name] = Set.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def collection(collection)
|
20
|
+
collection_name = if collection.respond_to?(:table_name)
|
21
|
+
collection.table_name.to_sym
|
22
|
+
else
|
23
|
+
collection.to_sym
|
24
|
+
end
|
25
|
+
|
26
|
+
@collections[collection_name]
|
27
|
+
end
|
28
|
+
|
29
|
+
def insert(collection, *values)
|
30
|
+
self.collection(collection).merge(*values)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Retrieve all records for +collection+ matching all +conditions+.
|
34
|
+
def all(collection, **conditions)
|
35
|
+
self.collection(collection).select do |row|
|
36
|
+
conditions.all? do |k,v|
|
37
|
+
row[k] === v
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Praxis::Mapper
|
4
|
+
module Support
|
5
|
+
class SchemaDumper
|
6
|
+
|
7
|
+
attr_reader :options, :repositories, :schema_root
|
8
|
+
|
9
|
+
def initialize(schema_root='.', **options)
|
10
|
+
@schema_root = Pathname.new(schema_root)
|
11
|
+
|
12
|
+
@options = options
|
13
|
+
@connection_manager = ConnectionManager.new
|
14
|
+
|
15
|
+
@repositories = Hash.new do |hash, repository_name|
|
16
|
+
hash[repository_name] = Set.new
|
17
|
+
end
|
18
|
+
|
19
|
+
setup!
|
20
|
+
end
|
21
|
+
|
22
|
+
def setup!
|
23
|
+
@connection_manager.repositories.each do |repository_name, config|
|
24
|
+
next unless config[:query] == Praxis::Mapper::Query::Sql
|
25
|
+
|
26
|
+
models = Praxis::Mapper::Model.descendants.
|
27
|
+
select { |model| model.repository_name == repository_name }.
|
28
|
+
select { |model| model.table_name }
|
29
|
+
|
30
|
+
models.each do |model|
|
31
|
+
table = model.table_name
|
32
|
+
repository = model.repository_name
|
33
|
+
self.repositories[repository] << table
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def dump_all!
|
39
|
+
repositories.each do |repository_name, tables|
|
40
|
+
self.dump!(repository_name)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def dump!(repository_name)
|
45
|
+
connection = @connection_manager.checkout(repository_name)
|
46
|
+
connection.extension :schema_dumper
|
47
|
+
|
48
|
+
tables = self.repositories.fetch repository_name
|
49
|
+
|
50
|
+
FileUtils.mkdir_p(schema_root + repository_name.to_s)
|
51
|
+
|
52
|
+
tables.each do |table|
|
53
|
+
File.open(schema_root + repository_name.to_s + "#{table}.rb" ,"w+") do |file|
|
54
|
+
file.puts "Sequel.migration do"
|
55
|
+
file.puts " up do"
|
56
|
+
file.puts connection.dump_table_schema(table).gsub(/^/o, ' ')
|
57
|
+
file.puts "\n"
|
58
|
+
file.puts " end"
|
59
|
+
file.puts "end"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|