praxis-mapper 3.1.1

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