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