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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +26 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +4 -0
  5. data/CHANGELOG.md +83 -0
  6. data/Gemfile +3 -0
  7. data/Gemfile.lock +102 -0
  8. data/Guardfile +11 -0
  9. data/LICENSE +22 -0
  10. data/README.md +19 -0
  11. data/Rakefile +14 -0
  12. data/lib/praxis-mapper/config_hash.rb +40 -0
  13. data/lib/praxis-mapper/connection_manager.rb +102 -0
  14. data/lib/praxis-mapper/finalizable.rb +38 -0
  15. data/lib/praxis-mapper/identity_map.rb +532 -0
  16. data/lib/praxis-mapper/logging.rb +22 -0
  17. data/lib/praxis-mapper/model.rb +430 -0
  18. data/lib/praxis-mapper/query/base.rb +213 -0
  19. data/lib/praxis-mapper/query/sql.rb +183 -0
  20. data/lib/praxis-mapper/query_statistics.rb +46 -0
  21. data/lib/praxis-mapper/resource.rb +226 -0
  22. data/lib/praxis-mapper/support/factory_girl.rb +104 -0
  23. data/lib/praxis-mapper/support/memory_query.rb +34 -0
  24. data/lib/praxis-mapper/support/memory_repository.rb +44 -0
  25. data/lib/praxis-mapper/support/schema_dumper.rb +66 -0
  26. data/lib/praxis-mapper/support/schema_loader.rb +56 -0
  27. data/lib/praxis-mapper/support.rb +2 -0
  28. data/lib/praxis-mapper/version.rb +5 -0
  29. data/lib/praxis-mapper.rb +60 -0
  30. data/praxis-mapper.gemspec +38 -0
  31. data/spec/praxis-mapper/connection_manager_spec.rb +117 -0
  32. data/spec/praxis-mapper/identity_map_spec.rb +905 -0
  33. data/spec/praxis-mapper/logging_spec.rb +9 -0
  34. data/spec/praxis-mapper/memory_repository_spec.rb +56 -0
  35. data/spec/praxis-mapper/model_spec.rb +389 -0
  36. data/spec/praxis-mapper/query/base_spec.rb +317 -0
  37. data/spec/praxis-mapper/query/sql_spec.rb +184 -0
  38. data/spec/praxis-mapper/resource_spec.rb +154 -0
  39. data/spec/praxis_mapper_spec.rb +21 -0
  40. data/spec/spec_fixtures.rb +12 -0
  41. data/spec/spec_helper.rb +63 -0
  42. data/spec/support/spec_models.rb +215 -0
  43. data/spec/support/spec_resources.rb +39 -0
  44. 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