legacy_data 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +49 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/examples/j2ee_petstore.sql +512 -0
- data/generators/models_from_tables/USAGE +6 -0
- data/generators/models_from_tables/models_from_tables_generator.rb +43 -0
- data/generators/models_from_tables/templates/model.rb +31 -0
- data/legacy_data.gemspec +69 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +61 -0
- data/lib/legacy_data/schema.rb +155 -0
- data/lib/legacy_data/table_class_name_mapper.rb +52 -0
- data/lib/legacy_data/table_definition.rb +41 -0
- data/lib/legacy_data.rb +4 -0
- data/spec/legacy_data/schema_spec.rb +179 -0
- data/spec/legacy_data/table_class_name_mapper_spec.rb +91 -0
- data/spec/legacy_data/table_definition_spec.rb +73 -0
- data/spec/spec_helper.rb +15 -0
- metadata +97 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../../lib/legacy_data/table_definition'
|
2
|
+
require File.dirname(__FILE__) + '/../../lib/legacy_data/schema'
|
3
|
+
require File.dirname(__FILE__) + '/../../lib/legacy_data/table_class_name_mapper'
|
4
|
+
require File.dirname(__FILE__) + '/../../lib/active_record/connection_adapters/oracle_enhanced_adapter'
|
5
|
+
|
6
|
+
class ModelsFromTablesGenerator < Rails::Generator::Base
|
7
|
+
def manifest
|
8
|
+
record do |m|
|
9
|
+
m.directory File.join('app/models')
|
10
|
+
|
11
|
+
LegacyData::TableClassNameMapper.naming_convention = options[:table_naming_convention]
|
12
|
+
|
13
|
+
analyzed_tables = LegacyData::Schema.analyze(:table_name=>options[:table_name])
|
14
|
+
LegacyData::TableClassNameMapper.save_dictionary
|
15
|
+
puts <<-MSG
|
16
|
+
Done analyzing the tables.
|
17
|
+
Please review the class names shown above. If any do not look correct (for example it did not separate the words with CamelCase) please supply the correct mappings by editing the file #{LegacyData::TableClassNameMapper.dictionary_file_name}.
|
18
|
+
Once you're done hit <enter> to continue generating the models"
|
19
|
+
MSG
|
20
|
+
gets
|
21
|
+
LegacyData::TableClassNameMapper.load_dictionary
|
22
|
+
|
23
|
+
analyzed_tables.each do |analyzed_table|
|
24
|
+
analyzed_table.class_name = LegacyData::TableClassNameMapper.class_name_for(analyzed_table[:table_name])
|
25
|
+
m.class_collisions :class_path, analyzed_table[:class_name]
|
26
|
+
m.template 'model.rb',
|
27
|
+
File.join('app/models', "#{analyzed_table[:class_name].underscore}.rb"),
|
28
|
+
:assigns => analyzed_table.to_hash
|
29
|
+
end
|
30
|
+
end
|
31
|
+
rescue => e
|
32
|
+
puts e.backtrace
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
def add_options!(opt)
|
37
|
+
opt.on('--table-name [ARG]',
|
38
|
+
'Only generate models for given table') { |value| options[:table_name] = value }
|
39
|
+
opt.on('--table-naming-convention [ARG]',
|
40
|
+
'Naming convention for tables in the database - will not be used when generating naming the models') { |value| options[:table_naming_convention] = value }
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class <%= class_name -%> < ActiveRecord::Base
|
2
|
+
set_table_name <%= table_name.to_sym.inspect %>
|
3
|
+
<%= "set_primary_key #{primary_key.to_sym.inspect}" if primary_key %>
|
4
|
+
|
5
|
+
# Relationships
|
6
|
+
<%- [:has_many, :has_one, :belongs_to, :has_and_belongs_to_many].each do |relation_type|
|
7
|
+
relations[relation_type].each do |table_name, options|
|
8
|
+
class_for_table = LegacyData::TableClassNameMapper.class_name_for(table_name)
|
9
|
+
is_singular_association = [:has_one, :belongs_to].include?(relation_type)
|
10
|
+
association_name = class_for_table.underscore
|
11
|
+
association_name = association_name.pluralize unless is_singular_association
|
12
|
+
needs_class_name = (ActiveRecord::Base.class_name(association_name.pluralize) != class_for_table)
|
13
|
+
-%> <%= relation_type %> <%= association_name.to_sym.inspect %>, <%=options.keys.map {|key| "#{key.to_sym.inspect} => #{options[key].to_sym.inspect}"}.join(', ')%><%= ", :class_name=>'#{class_for_table}'" if needs_class_name %>
|
14
|
+
<%- end unless relations[relation_type].nil?
|
15
|
+
end
|
16
|
+
-%>
|
17
|
+
|
18
|
+
# Constraints
|
19
|
+
<%= "validates_uniqueness_of #{constraints[:unique ].map {|cols| cols.first.downcase.to_sym.inspect}.join(', ')}" unless constraints[:unique].blank? %>
|
20
|
+
<%- constraints[:multi_column_unique].each do |cols|
|
21
|
+
-%> #validates_uniqueness_of_multiple_column_constraint :<%= cols.inspect %>
|
22
|
+
<%- end -%>
|
23
|
+
<%= "validates_presence_of #{constraints[:non_nullable].map {|cols| cols.downcase.to_sym.inspect}.join(', ')}" unless constraints[:non_nullable].blank? %>
|
24
|
+
<%- constraints[:custom].each do |name, sql_rule|
|
25
|
+
-%> validate <%= "validate_#{name}".to_sym.inspect %>
|
26
|
+
def <%= "validate_#{name}" %>
|
27
|
+
# TODO: validate this SQL constraint
|
28
|
+
"<%= sql_rule %>"
|
29
|
+
end
|
30
|
+
<%- end %>
|
31
|
+
end
|
data/legacy_data.gemspec
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{legacy_data}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Alex Rothenberg"]
|
12
|
+
s.date = %q{2009-10-01}
|
13
|
+
s.description = %q{Create ActiveRecord models from an existing database}
|
14
|
+
s.email = %q{alex@alexrothenberg.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"examples/j2ee_petstore.sql",
|
27
|
+
"generators/models_from_tables/USAGE",
|
28
|
+
"generators/models_from_tables/models_from_tables_generator.rb",
|
29
|
+
"generators/models_from_tables/templates/model.rb",
|
30
|
+
"legacy_data.gemspec",
|
31
|
+
"lib/active_record/connection_adapters/oracle_enhanced_adapter.rb",
|
32
|
+
"lib/legacy_data.rb",
|
33
|
+
"lib/legacy_data/schema.rb",
|
34
|
+
"lib/legacy_data/table_class_name_mapper.rb",
|
35
|
+
"lib/legacy_data/table_definition.rb",
|
36
|
+
"spec/legacy_data/schema_spec.rb",
|
37
|
+
"spec/legacy_data/table_class_name_mapper_spec.rb",
|
38
|
+
"spec/legacy_data/table_definition_spec.rb",
|
39
|
+
"spec/spec_helper.rb"
|
40
|
+
]
|
41
|
+
s.has_rdoc = true
|
42
|
+
s.homepage = %q{http://github.com/alexrothenberg/legacy_data}
|
43
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
44
|
+
s.require_paths = ["lib"]
|
45
|
+
s.rubygems_version = %q{1.3.2}
|
46
|
+
s.summary = %q{Create ActiveRecord models from an existing database}
|
47
|
+
s.test_files = [
|
48
|
+
"spec/legacy_data/schema_spec.rb",
|
49
|
+
"spec/legacy_data/table_class_name_mapper_spec.rb",
|
50
|
+
"spec/legacy_data/table_definition_spec.rb",
|
51
|
+
"spec/spec_helper.rb"
|
52
|
+
]
|
53
|
+
|
54
|
+
if s.respond_to? :specification_version then
|
55
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
56
|
+
s.specification_version = 3
|
57
|
+
|
58
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
59
|
+
s.add_development_dependency(%q<rspec>, [">= 0"])
|
60
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 0"])
|
61
|
+
else
|
62
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
63
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
64
|
+
end
|
65
|
+
else
|
66
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
67
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module OracleEnhancedAdapterConstraintsMethods
|
2
|
+
|
3
|
+
def foreign_keys_for(table_name)
|
4
|
+
(owner, table_name) = @connection.describe(table_name)
|
5
|
+
|
6
|
+
# RSI: changed select from all_constraints to user_constraints - much faster in large data dictionaries
|
7
|
+
fks = select_rows(<<-SQL, 'Foreign Keys')
|
8
|
+
select parent_c.table_name, cc.column_name
|
9
|
+
from user_constraints c, user_constraints parent_c, user_cons_columns cc
|
10
|
+
where c.owner = '#{owner}'
|
11
|
+
and c.table_name = '#{table_name}'
|
12
|
+
and c.r_constraint_name = parent_c.constraint_name
|
13
|
+
and c.constraint_type = 'R'
|
14
|
+
and cc.owner = c.owner
|
15
|
+
and cc.constraint_name = c.constraint_name
|
16
|
+
SQL
|
17
|
+
end
|
18
|
+
def foreign_keys_of(table_name)
|
19
|
+
(owner, table_name) = @connection.describe(table_name)
|
20
|
+
|
21
|
+
# RSI: changed select from all_constraints to user_constraints - much faster in large data dictionaries
|
22
|
+
fks = select_rows(<<-SQL, 'Remote Foriegn Keys')
|
23
|
+
select c.table_name, cc.column_name, c.delete_rule
|
24
|
+
from user_constraints c, user_constraints parent_c, user_cons_columns cc
|
25
|
+
where c.owner = '#{owner}'
|
26
|
+
and parent_c.table_name = '#{table_name}'
|
27
|
+
and c.r_constraint_name = parent_c.constraint_name
|
28
|
+
and c.constraint_type = 'R'
|
29
|
+
and cc.owner = c.owner
|
30
|
+
and cc.constraint_name = c.constraint_name
|
31
|
+
SQL
|
32
|
+
fks.map do |row|
|
33
|
+
dependent = case row[2]
|
34
|
+
when 'CASCADE'
|
35
|
+
:destroy
|
36
|
+
when 'SET NULL'
|
37
|
+
:nullify
|
38
|
+
end
|
39
|
+
options = {:to_table => row[0].downcase, :foreign_key =>row[1].downcase.to_sym }
|
40
|
+
options[:dependent] = dependent unless dependent.nil?
|
41
|
+
options
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def constraints(table_name)
|
46
|
+
(owner, table_name) = @connection.describe(table_name)
|
47
|
+
|
48
|
+
# RSI: changed select from all_constraints to user_constraints - much faster in large data dictionaries
|
49
|
+
fks = select_rows(<<-SQL, 'User Contraints')
|
50
|
+
select c.constraint_name, c.search_condition
|
51
|
+
from user_constraints c
|
52
|
+
where c.owner = '#{owner}'
|
53
|
+
and c.table_name = '#{table_name}'
|
54
|
+
and c.constraint_type = 'C'
|
55
|
+
and c.generated = 'USER NAME'
|
56
|
+
and c.status = 'ENABLED'
|
57
|
+
SQL
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.send(:include, OracleEnhancedAdapterConstraintsMethods) if defined? ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module LegacyData
|
2
|
+
class Schema
|
3
|
+
attr_reader :table_name
|
4
|
+
|
5
|
+
def self.analyze(options={})
|
6
|
+
initialize_tables(options[:table_name])
|
7
|
+
|
8
|
+
while table_name = next_table_to_process
|
9
|
+
table_definitions[table_name] = analyze_table(table_name)
|
10
|
+
|
11
|
+
[:has_many, :belongs_to].each do |relation_type|
|
12
|
+
associated_tables = table_definitions[table_name][:relations][relation_type].keys.map(&:to_s)
|
13
|
+
associated_tables.each {|associated_table| add_pending_table(associated_table) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
remove_join_tables
|
18
|
+
end
|
19
|
+
def self.analyze_table table_name
|
20
|
+
new(table_name).analyze_table
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def self.initialize_tables(table_name)
|
25
|
+
clear_table_definitions
|
26
|
+
if table_name
|
27
|
+
add_pending_table(table_name)
|
28
|
+
else
|
29
|
+
LegacyData::Schema.tables.each {|table| add_pending_table(table) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
def self.add_pending_table table_name
|
33
|
+
table_definitions[table_name] = :pending if table_definitions[table_name].nil?
|
34
|
+
end
|
35
|
+
def self.next_table_to_process
|
36
|
+
table_definitions.keys.detect {|table_name| table_definitions[table_name] == :pending }
|
37
|
+
end
|
38
|
+
def self.clear_table_definitions
|
39
|
+
@tables = {}
|
40
|
+
end
|
41
|
+
def self.table_definitions
|
42
|
+
@tables ||= {}
|
43
|
+
end
|
44
|
+
def self.next_join_table
|
45
|
+
table_definitions.keys.detect {|table_name| table_definitions[table_name].join_table? }
|
46
|
+
end
|
47
|
+
def self.remove_join_tables
|
48
|
+
join_tables, other_tables = table_definitions.values.partition &:join_table?
|
49
|
+
|
50
|
+
join_tables.each { |join_table| convert_to_habtm(join_table) }
|
51
|
+
|
52
|
+
other_tables
|
53
|
+
end
|
54
|
+
def self.convert_to_habtm join_table
|
55
|
+
join_table.belongs_to_tables.each do |table|
|
56
|
+
table_definitions[table].convert_has_many_to_habtm(join_table)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize(table_name)
|
61
|
+
@table_name = table_name
|
62
|
+
end
|
63
|
+
|
64
|
+
def analyze_table
|
65
|
+
puts "analyzing #{table_name} => #{class_name}"
|
66
|
+
TableDefinition.new(:table_name => table_name,
|
67
|
+
:columns => column_names,
|
68
|
+
:primary_key => primary_key,
|
69
|
+
:relations => relations,
|
70
|
+
:constraints => constraints
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.tables
|
75
|
+
connection.tables.sort
|
76
|
+
end
|
77
|
+
|
78
|
+
def class_name
|
79
|
+
TableClassNameMapper.class_name_for(self.table_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def primary_key
|
83
|
+
pk_and_sequence_for = connection.pk_and_sequence_for(table_name)
|
84
|
+
pk_and_sequence_for.first if pk_and_sequence_for.respond_to? :first
|
85
|
+
end
|
86
|
+
|
87
|
+
def relations
|
88
|
+
{ :belongs_to => belongs_to_relations,
|
89
|
+
:has_many => has_some_relations,
|
90
|
+
:has_and_belongs_to_many => {}
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def belongs_to_relations
|
95
|
+
return [] unless connection.respond_to? :foreign_keys_for
|
96
|
+
|
97
|
+
belongs_to = {}
|
98
|
+
connection.foreign_keys_for(table_name).each do |relation|
|
99
|
+
belongs_to[relation.first.downcase] = {:foreign_key=>relation.second.downcase.to_sym}
|
100
|
+
end
|
101
|
+
belongs_to
|
102
|
+
end
|
103
|
+
def has_some_relations
|
104
|
+
return [] unless connection.respond_to? :foreign_keys_of
|
105
|
+
|
106
|
+
has_some = {}
|
107
|
+
connection.foreign_keys_of(table_name).each do |relation|
|
108
|
+
has_some[relation.delete(:to_table).downcase] = relation
|
109
|
+
end
|
110
|
+
has_some
|
111
|
+
end
|
112
|
+
|
113
|
+
def constraints
|
114
|
+
unique, multi_column_unique = unique_constraints.partition {|columns| columns.size == 1}
|
115
|
+
{ :unique => unique,
|
116
|
+
:multi_column_unique => multi_column_unique,
|
117
|
+
:non_nullable => non_nullable_constraints,
|
118
|
+
:custom => custom_constraints
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
def columns
|
123
|
+
@columns ||= connection.columns(table_name, "#{table_name} Columns")
|
124
|
+
end
|
125
|
+
def column_names
|
126
|
+
columns.map(&:name)
|
127
|
+
end
|
128
|
+
def non_nullable_constraints
|
129
|
+
non_nullable_constraints = columns.reject(&:null).map(&:name)
|
130
|
+
non_nullable_constraints.reject {|col| col == primary_key}
|
131
|
+
end
|
132
|
+
|
133
|
+
def unique_constraints
|
134
|
+
connection.indexes(table_name).select(&:unique).map(&:columns)
|
135
|
+
end
|
136
|
+
|
137
|
+
def custom_constraints
|
138
|
+
return [] unless connection.respond_to? :constraints
|
139
|
+
user_constraints = {}
|
140
|
+
connection.constraints(table_name).each do |constraint|
|
141
|
+
user_constraints[constraint.first.underscore.to_sym] = constraint.second
|
142
|
+
end
|
143
|
+
user_constraints
|
144
|
+
end
|
145
|
+
private
|
146
|
+
def self.connection
|
147
|
+
@conn ||= ActiveRecord::Base.connection
|
148
|
+
end
|
149
|
+
def connection
|
150
|
+
self.class.connection
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module LegacyData
|
2
|
+
class TableClassNameMapper
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
attr_accessor :naming_convention, :dictionary
|
6
|
+
def naming_convention= naming_convention
|
7
|
+
@naming_convention = (naming_convention || '*').gsub('*', '(.*)')
|
8
|
+
end
|
9
|
+
|
10
|
+
def dictionary
|
11
|
+
@dictionary ||= load_dictionary
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.method_missing(method_id, *arguments, &block)
|
15
|
+
instance.send(method_id, *arguments, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def clear_dictionary
|
19
|
+
@dictionary = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def load_dictionary
|
23
|
+
clear_dictionary
|
24
|
+
File.exists?(dictionary_file_name) ? YAML.load_file(dictionary_file_name) : {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def save_dictionary
|
28
|
+
File.open(dictionary_file_name, 'w') do |out|
|
29
|
+
YAML.dump(dictionary, out)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def dictionary_file_name
|
34
|
+
File.join(RAILS_ROOT, 'app', 'models', 'table_mappings.yml')
|
35
|
+
end
|
36
|
+
|
37
|
+
def class_name_for table_name
|
38
|
+
lookup_class_name(table_name) || compute_class_name(table_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def lookup_class_name table_name
|
42
|
+
dictionary[table_name]
|
43
|
+
end
|
44
|
+
|
45
|
+
def compute_class_name table_name
|
46
|
+
table_name =~ /#{naming_convention}/i
|
47
|
+
stripped_table_name = $1 || table_name
|
48
|
+
dictionary[table_name] = ActiveRecord::Base.class_name(stripped_table_name.downcase.pluralize)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module LegacyData
|
2
|
+
class TableDefinition
|
3
|
+
attr_accessor :class_name, :table_name, :columns, :primary_key, :relations, :constraints
|
4
|
+
|
5
|
+
def initialize(options)
|
6
|
+
options.each {|key, value| self.send("#{key}=", value) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def [] key
|
10
|
+
self.send(key)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_hash
|
14
|
+
hash = {}
|
15
|
+
[:class_name, :table_name, :columns, :primary_key, :relations, :constraints].each {|field| hash[field] = self.send(field) }
|
16
|
+
hash
|
17
|
+
end
|
18
|
+
|
19
|
+
def join_table?
|
20
|
+
(columns.size == 2) and relations[:belongs_to] and (relations[:belongs_to].values.map {|value| value[:foreign_key]}.map(&:to_s) == columns)
|
21
|
+
end
|
22
|
+
|
23
|
+
def belongs_to_relations
|
24
|
+
return {} if relations.nil? or relations[:belongs_to].nil?
|
25
|
+
relations[:belongs_to]
|
26
|
+
end
|
27
|
+
|
28
|
+
def belongs_to_tables
|
29
|
+
return [] if belongs_to_relations == {}
|
30
|
+
belongs_to_relations.keys
|
31
|
+
end
|
32
|
+
|
33
|
+
def convert_has_many_to_habtm(join_table)
|
34
|
+
other_table_name = join_table.belongs_to_tables.detect {|table_name| table_name != self.table_name}
|
35
|
+
relations[:has_and_belongs_to_many][other_table_name] = { :foreign_key =>join_table.belongs_to_relations[table_name][:foreign_key],
|
36
|
+
:association_foreign_key=>join_table.belongs_to_relations[other_table_name][:foreign_key],
|
37
|
+
:join_table =>join_table.table_name.to_sym }
|
38
|
+
relations[:has_many].delete(join_table.table_name)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/legacy_data.rb
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe LegacyData::Schema do
|
4
|
+
describe 'following associations' do
|
5
|
+
before :each do
|
6
|
+
LegacyData::Schema.stub!(:analyze_table).with('posts' ).and_return(@posts_analysis =mock(:posts, {:join_table? =>false}))
|
7
|
+
LegacyData::Schema.stub!(:analyze_table).with('comments').and_return(@comments_analysis=mock(:comments, {:join_table? =>false}))
|
8
|
+
@posts_analysis.stub!( :[]).with(:relations).and_return({:belongs_to=>{ }, :has_many=>{:comments=>{:foreign_key=>:posts_id}}})
|
9
|
+
@comments_analysis.stub!(:[]).with(:relations).and_return({:belongs_to=>{:posts=>{:foreign_key=>:posts_id}}, :has_many=>{ }})
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should analyze all tables when not given a table to start with' do
|
13
|
+
LegacyData::Schema.should_receive(:tables).and_return(['posts', 'comments'])
|
14
|
+
|
15
|
+
LegacyData::Schema.analyze.should include(@posts_analysis, @comments_analysis)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should find the associated comments when starting with posts' do
|
19
|
+
LegacyData::Schema.analyze(:table_name=>'posts').should include(@posts_analysis, @comments_analysis)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should analyze a single table' do
|
24
|
+
LegacyData::Schema.should_receive(:new).with('some_table').and_return(schema=mock)
|
25
|
+
schema.should_receive(:analyze_table).and_return(analysis=mock)
|
26
|
+
|
27
|
+
LegacyData::Schema.analyze_table('some_table').should == analysis
|
28
|
+
end
|
29
|
+
|
30
|
+
before :each do
|
31
|
+
LegacyData::Schema.instance_variable_set('@conn', nil)
|
32
|
+
ActiveRecord::Base.stub(:connection).and_return(@connection=stub(:connection))
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should get a list of the tables from the database" do
|
36
|
+
@connection.should_receive(:tables).and_return(['comments', 'people', 'posts'])
|
37
|
+
LegacyData::Schema.tables.should == ['comments', 'people', 'posts']
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'analyzing a table' do
|
41
|
+
before :each do
|
42
|
+
@schema = LegacyData::Schema.new('some_table')
|
43
|
+
@schema.stub!(:puts)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should have all the information about the table' do
|
47
|
+
@schema.stub!(:class_name ).and_return(class_name =mock)
|
48
|
+
@schema.stub!(:column_names).and_return(columns =mock)
|
49
|
+
@schema.stub!(:primary_key ).and_return(primary_key=mock)
|
50
|
+
@schema.stub!(:relations ).and_return(relations =mock)
|
51
|
+
@schema.stub!(:constraints ).and_return(constraints=mock)
|
52
|
+
@schema.analyze_table[:table_name ].should == 'some_table'
|
53
|
+
@schema.analyze_table[:columns ].should == columns
|
54
|
+
@schema.analyze_table[:primary_key].should == primary_key
|
55
|
+
@schema.analyze_table[:relations ].should == relations
|
56
|
+
@schema.analyze_table[:constraints].should == constraints
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should have the correct table name' do
|
60
|
+
@schema.table_name.should == 'some_table'
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should have the correct class name' do
|
64
|
+
LegacyData::TableClassNameMapper.should_receive(:class_name_for).with('some_table').and_return('SomeClass')
|
65
|
+
@schema.class_name.should == 'SomeClass'
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should have the correct primary_key' do
|
69
|
+
@connection.should_receive(:pk_and_sequence_for).with('some_table').and_return(['PK', 'SEQ'])
|
70
|
+
@schema.primary_key.should == 'PK'
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should have the names of all columns' do
|
74
|
+
@schema.should_receive(:columns).and_return([col1=mock(:col1, :name=>'Col1'), col2=mock(:col2, :name=>'Col2')])
|
75
|
+
@schema.column_names.sort.should == ['Col1', 'Col2']
|
76
|
+
end
|
77
|
+
|
78
|
+
describe 'relations' do
|
79
|
+
it 'should have the different types of relations' do
|
80
|
+
@schema.stub!(:belongs_to_relations).and_return([belongs_to=mock])
|
81
|
+
@schema.stub!(:has_some_relations ).and_return([has_some =mock])
|
82
|
+
|
83
|
+
@schema.relations.should == {:belongs_to=>[belongs_to], :has_many=>[has_some], :has_and_belongs_to_many=>{}}
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
it 'should give no "has_some" (has_many and has_one) relationships when the adapter does not support foreign keys' do
|
88
|
+
@connection.should_receive(:respond_to?).with(:foreign_keys_of).and_return(false)
|
89
|
+
@schema.has_some_relations.should == []
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should get all "has_some" (has_many and has_one) relationships when my primary key is the foreign key in another table ' do
|
93
|
+
@connection.should_receive(:respond_to?).with(:foreign_keys_of).and_return(true)
|
94
|
+
@connection.should_receive(:foreign_keys_of).and_return([ {:to_table=>'other_table', :foreign_key=>:pk, :dependent=>:destroy},
|
95
|
+
{:to_table=>'the_table', :foreign_key=>:pk, :dependent=>:destroy} ])
|
96
|
+
@schema.has_some_relations.should == {'other_table' => {:foreign_key=>:pk, :dependent=>:destroy},
|
97
|
+
'the_table' => {:foreign_key=>:pk, :dependent=>:destroy}}
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should give no "belongs_to" when the adapter does not support foreign keys' do
|
101
|
+
@connection.should_receive(:respond_to?).with(:foreign_keys_for).and_return(false)
|
102
|
+
@schema.belongs_to_relations.should == []
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should get all "belongs_to" relationships when a foreign key is in my table' do
|
106
|
+
@connection.should_receive(:respond_to?).with(:foreign_keys_for).and_return(true)
|
107
|
+
@connection.should_receive(:foreign_keys_for).and_return([['OTHER_TABLE', 'FK_1'], ['THE_TABLE', 'the_table_id']])
|
108
|
+
@schema.belongs_to_relations.should == {'other_table' => {:foreign_key=>:fk_1 },
|
109
|
+
'the_table' => {:foreign_key=>:the_table_id} }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe 'constraints' do
|
114
|
+
it 'should have the different types of constraints' do
|
115
|
+
@schema.stub!(:unique_constraints ).and_return([['unique_col'], ['col1', 'col2']])
|
116
|
+
@schema.stub!(:non_nullable_constraints).and_return([non_nullable=mock ])
|
117
|
+
@schema.stub!(:custom_constraints ).and_return([custom =mock ])
|
118
|
+
|
119
|
+
@schema.constraints.should == {:unique=>[['unique_col']], :multi_column_unique=>[['col1', 'col2']], :non_nullable=>[non_nullable], :custom=>[custom]}
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should get non-nullable constraints as all columns that do not allow null except the primary key' do
|
123
|
+
@schema.stub(:primary_key).and_return('col1')
|
124
|
+
@connection.should_receive(:columns).with('some_table', 'some_table Columns').and_return([col1=stub(:null=>false, :name=>'col1'),
|
125
|
+
col2=stub(:null=>false, :name=>'col2'),
|
126
|
+
col3=stub(:null=>true, :name=>'col3'),
|
127
|
+
col3=stub(:null=>false, :name=>'col4')])
|
128
|
+
@schema.non_nullable_constraints.should == ['col2', 'col4']
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'should get uniqueness constraints' do
|
132
|
+
@connection.should_receive(:indexes).with('some_table').and_return([idx1=stub(:unique=>true, :columns=>['col1']),
|
133
|
+
idx2=stub(:unique=>false, :columns=>['col2']),
|
134
|
+
idx3=stub(:unique=>true, :columns=>['col3', 'col4'])])
|
135
|
+
@schema.unique_constraints.should == [['col1'], ['col3', 'col4']]
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should give no custom constraints when the adapter does not support it' do
|
139
|
+
@connection.should_receive(:respond_to?).with(:constraints).and_return(false)
|
140
|
+
@schema.custom_constraints.should == []
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should get all "belongs_to" relationships when a foreign key is in my table' do
|
144
|
+
@connection.should_receive(:respond_to?).with(:constraints).and_return(true)
|
145
|
+
@connection.should_receive(:constraints).and_return([['SomeConstraint', 'custom sql 1'], ['anotherconstraint', 'more custom sql']])
|
146
|
+
@schema.custom_constraints.should == {:some_constraint => 'custom sql 1', :anotherconstraint => 'more custom sql'}
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe 'convert join tables into HABTM relations' do
|
152
|
+
before :each do
|
153
|
+
LegacyData::Schema.clear_table_definitions
|
154
|
+
LegacyData::Schema.table_definitions['posts' ] = @posts_table =mock(:posts, :join_table? => false)
|
155
|
+
LegacyData::Schema.table_definitions['tags' ] = @tags_table =mock(:tags, :join_table? => false)
|
156
|
+
LegacyData::Schema.table_definitions['tag_posts'] = @tag_posts_join_table=mock(:tag_posts, :join_table? => true )
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'should remove the join table from the list of tables' do
|
160
|
+
@tag_posts_join_table.stub!(:belongs_to_tables).and_return(['posts', 'tags'])
|
161
|
+
@posts_table.should_receive(:convert_has_many_to_habtm).with(@tag_posts_join_table)
|
162
|
+
@tags_table.should_receive( :convert_has_many_to_habtm).with(@tag_posts_join_table)
|
163
|
+
|
164
|
+
analyzed_tables = LegacyData::Schema.remove_join_tables
|
165
|
+
|
166
|
+
analyzed_tables.should include(@posts_table, @tags_table)
|
167
|
+
analyzed_tables.size.should == 2
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'should find the next join table' do
|
171
|
+
LegacyData::Schema.next_join_table.should == 'tag_posts'
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'should know when there are no join tables' do
|
175
|
+
LegacyData::Schema.table_definitions.delete('tag_posts')
|
176
|
+
LegacyData::Schema.next_join_table.should == nil
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|