dr_nic_magic_models 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +9 -9
- data/lib/base.rb +10 -95
- data/lib/connection_adapters/abstract/schema_statements.rb +0 -0
- data/lib/connection_adapters/abstract_adapter.rb +32 -0
- data/lib/connection_adapters/mysql_adapter.rb +42 -0
- data/lib/connection_adapters/postgresql_adapter.rb +45 -0
- data/lib/dr_nic_magic_models/schema.rb +210 -8
- data/lib/dr_nic_magic_models/validations.rb +35 -18
- data/lib/dr_nic_magic_models/version.rb +2 -2
- data/lib/dr_nic_magic_models.rb +13 -2
- data/lib/module.rb +0 -18
- data/lib/rails.rb +19 -0
- data/lib/schema_dumper.rb +0 -0
- data/scripts/txt2js +57 -0
- data/test/connections/native_postgresql/connection.rb +14 -0
- data/test/fixtures/adjectives.yml +3 -0
- data/test/fixtures/adjectives_fun_users.yml +3 -0
- data/test/fixtures/db_definitions/mysql.drop.sql +4 -30
- data/test/fixtures/db_definitions/mysql.sql +30 -2
- data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
- data/test/fixtures/db_definitions/postgresql.sql +56 -0
- data/test/fixtures/fun_users.yml +8 -1
- data/test/fixtures/group_tag.yml +3 -1
- data/test/fixtures/groups.yml +9 -1
- data/test/foreign_keys_test.rb +0 -0
- data/test/fun_user_plus.rb +2 -0
- data/test/invisible_model_access_test.rb +35 -7
- data/test/invisible_model_assoc_test.rb +34 -19
- data/test/invisible_model_classes_test.rb +5 -1
- data/website/index.html +24 -4
- data/website/index.txt +9 -1
- data/website/template.js +3 -0
- data/website/template.rhtml +10 -1
- data/website/version-raw.js +3 -0
- data/website/version-raw.txt +1 -0
- data/website/version.js +4 -0
- data/website/version.txt +3 -0
- metadata +32 -19
- data/lib/dr_nic_magic_models/associations.rb +0 -12
data/Rakefile
CHANGED
@@ -18,7 +18,7 @@ RUBY_FORGE_PROJECT = "magicmodels"
|
|
18
18
|
RUBY_FORGE_USER = "nicwilliams"
|
19
19
|
|
20
20
|
PKG_FILES = FileList[
|
21
|
-
"lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "website/**/*", "[A-Z]*", "install.rb", "Rakefile"
|
21
|
+
"lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "website/**/*", "scripts/**/*", "[A-Z]*", "install.rb", "Rakefile"
|
22
22
|
].exclude(/\bCVS\b|~$/)
|
23
23
|
|
24
24
|
|
@@ -28,7 +28,7 @@ task :test => [ :test_mysql ]
|
|
28
28
|
|
29
29
|
# Run the unit tests
|
30
30
|
|
31
|
-
for adapter in %w( mysql ) # UNTESTED - postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase openbase )
|
31
|
+
for adapter in %w( mysql postgresql ) # UNTESTED - postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase openbase )
|
32
32
|
Rake::TestTask.new("test_#{adapter}") { |t|
|
33
33
|
t.libs << "test" << "test/connections/native_#{adapter}"
|
34
34
|
t.pattern = "test/*_test{,_#{adapter}}.rb"
|
@@ -42,7 +42,9 @@ desc 'Build the MySQL test databases'
|
|
42
42
|
task :build_mysql_databases do
|
43
43
|
puts File.join(SCHEMA_PATH, 'mysql.sql')
|
44
44
|
%x( mysqladmin -u root create "#{PKG_NAME}_unittest" )
|
45
|
-
|
45
|
+
cmd = "mysql -u root #{PKG_NAME}_unittest < \"#{File.join(SCHEMA_PATH, 'mysql.sql')}\""
|
46
|
+
puts "#{cmd}\n"
|
47
|
+
%x( #{cmd} )
|
46
48
|
end
|
47
49
|
|
48
50
|
desc 'Drop the MySQL test databases'
|
@@ -55,13 +57,13 @@ task :rebuild_mysql_databases => [:drop_mysql_databases, :build_mysql_databases]
|
|
55
57
|
|
56
58
|
desc 'Build the PostgreSQL test databases'
|
57
59
|
task :build_postgresql_databases do
|
58
|
-
%x( createdb "#{PKG_NAME}_unittest" )
|
59
|
-
%x( psql "#{PKG_NAME}_unittest" -f #{File.join(SCHEMA_PATH, 'postgresql.sql')} )
|
60
|
+
%x( createdb -U postgres "#{PKG_NAME}_unittest" )
|
61
|
+
%x( psql "#{PKG_NAME}_unittest" postgres -f "#{File.join(SCHEMA_PATH, 'postgresql.sql')}" )
|
60
62
|
end
|
61
63
|
|
62
64
|
desc 'Drop the PostgreSQL test databases'
|
63
65
|
task :drop_postgresql_databases do
|
64
|
-
%x( dropdb
|
66
|
+
%x( dropdb -U postgres "#{PKG_NAME}_unittest" )
|
65
67
|
end
|
66
68
|
|
67
69
|
desc 'Rebuild the PostgreSQL test databases'
|
@@ -98,8 +100,6 @@ spec = Gem::Specification.new do |s|
|
|
98
100
|
dist_dirs.each do |dir|
|
99
101
|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
100
102
|
end
|
101
|
-
|
102
|
-
s.add_dependency('activerecord', '>= 1.14.3' + PKG_BUILD)
|
103
103
|
|
104
104
|
s.require_path = 'lib'
|
105
105
|
s.autorequire = 'dr_nic_magic_models'
|
@@ -156,4 +156,4 @@ task :release => [ :package ] do
|
|
156
156
|
puts release_command
|
157
157
|
system(release_command)
|
158
158
|
end
|
159
|
-
end
|
159
|
+
end
|
data/lib/base.rb
CHANGED
@@ -1,101 +1,16 @@
|
|
1
|
-
#
|
1
|
+
#TODO: Use :dependent for FK cascade?
|
2
|
+
|
2
3
|
module ActiveRecord
|
3
4
|
class Base
|
4
5
|
class << self
|
5
|
-
alias old_new new
|
6
|
-
def new(*args)
|
7
|
-
obj = old_new(*args)
|
8
|
-
unless self.methods.include? 'generate_validations'
|
9
|
-
self.send(:include, DrNicMagicModels::Validations)
|
10
|
-
self.generate_validations
|
11
|
-
end
|
12
|
-
obj
|
13
|
-
end
|
14
|
-
alias old_allocate allocate
|
15
|
-
def allocate
|
16
|
-
obj = old_allocate
|
17
|
-
unless self.methods.include? 'generate_validations'
|
18
|
-
self.send(:include, DrNicMagicModels::Validations)
|
19
|
-
self.generate_validations
|
20
|
-
end
|
21
|
-
obj
|
22
|
-
end
|
23
|
-
|
24
|
-
# Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
|
25
|
-
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
|
26
|
-
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
|
27
|
-
def reflect_on_association(association)
|
28
|
-
unless reflections[association]
|
29
|
-
# See if an assocation can be generated
|
30
|
-
self.new.send(association) rescue nil
|
31
|
-
end
|
32
|
-
reflections[association].is_a?(ActiveRecord::Reflection::AssociationReflection) ? reflections[association] : nil
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
class_eval do
|
37
|
-
alias :normal_method_missing :method_missing
|
38
|
-
def method_missing(method, *args, &block)
|
39
|
-
if unknown_method? method
|
40
|
-
result = find_belongs_to method, *args, &block
|
41
|
-
result = find_has_some method, *args, &block if not result
|
42
|
-
result = find_has_some_indirect method, *args, &block if not result
|
43
|
-
return result if result
|
44
|
-
end
|
45
|
-
add_known_unknown method
|
46
|
-
normal_method_missing(method, *args, &block)
|
47
|
-
end
|
48
|
-
|
49
|
-
def add_known_unknown(method)
|
50
|
-
@known_unknowns ||= {}
|
51
|
-
@known_unknowns[method] = true
|
52
|
-
end
|
53
|
-
|
54
|
-
def unknown_method?(method)
|
55
|
-
@known_unknowns.nil? or @known_unknowns.include? method
|
56
|
-
end
|
57
|
-
|
58
|
-
def find_belongs_to(method, *args, &block)
|
59
|
-
foreign_key = self.class.columns.select {|column| column.name == method.to_s.foreign_key}.first
|
60
|
-
return add_belongs_to(method, *args, &block) if foreign_key
|
61
|
-
end
|
62
6
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
def find_has_some(method, *args, &block)
|
69
|
-
klass = Module.const_get method.to_s.downcase.singularize.camelize rescue nil
|
70
|
-
foreign_key = klass.columns.select {|column| column.name == self.class.name.foreign_key}.first if klass
|
71
|
-
return add_has_some(method, *args, &block) if foreign_key
|
72
|
-
end
|
73
|
-
|
74
|
-
def add_has_some(method, *args, &block)
|
75
|
-
_method = method.to_s
|
76
|
-
association = _method.singularize == _method ? 'has_one' : 'has_many'
|
77
|
-
self.class.send association, method
|
78
|
-
self.send(method, *args, &block)
|
79
|
-
end
|
80
|
-
|
81
|
-
def find_has_some_indirect(method, *args, &block)
|
82
|
-
klass = Module.const_get method.to_s.downcase.singularize.camelize rescue return
|
83
|
-
join_table = nil
|
84
|
-
self.connection.tables.each do |table|
|
85
|
-
unless [self.class.table_name, klass.table_name].include? table
|
86
|
-
columns = self.connection.columns(table).map(&:name)
|
87
|
-
join_table = table if columns.include?(self.class.to_s.foreign_key) and columns.include?(klass.to_s.foreign_key)
|
88
|
-
end
|
89
|
-
break if join_table
|
90
|
-
end
|
91
|
-
return add_has_some_through(join_table, method, *args, &block) if join_table
|
92
|
-
end
|
93
|
-
|
94
|
-
def add_has_some_through(join_table, method, *args, &block)
|
95
|
-
self.class.send 'has_many', method, :through => join_table.to_sym
|
96
|
-
self.send(method, *args, &block)
|
97
|
-
end
|
98
|
-
|
7
|
+
public
|
8
|
+
|
9
|
+
def get_unique_index_columns
|
10
|
+
self.connection.indexes(self.table_name, "#{self.name} Indexes").select { |index| index.unique && index.columns.size == 1 }.map{ |index| index.columns.first }
|
11
|
+
end
|
99
12
|
end
|
13
|
+
|
14
|
+
|
100
15
|
end
|
101
|
-
end
|
16
|
+
end
|
File without changes
|
@@ -0,0 +1,32 @@
|
|
1
|
+
|
2
|
+
module ActiveRecord
|
3
|
+
module ConnectionAdapters # :nodoc:
|
4
|
+
|
5
|
+
# Generic holder for foreign key constraint meta-data from the database schema.
|
6
|
+
class ForeignKeyConstraint < Struct.new(:name, :table, :foreign_key, :reference_table, :reference_column, :on_update, :on_delete)
|
7
|
+
end
|
8
|
+
|
9
|
+
class AbstractAdapter
|
10
|
+
|
11
|
+
# Does this adapter support the ability to fetch foreign key information?
|
12
|
+
# Backend specific, as the abstract adapter always returns +false+.
|
13
|
+
def supports_fetch_foreign_keys?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def foreign_key_constraints(table, name = nil)
|
18
|
+
raise NotImplementedError, "foreign_key_constraints is not implemented for #{self.class}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def remove_foreign_key_constraint(table_name, constraint_name)
|
22
|
+
raise NotImplementedError, "rename_table is not implemented for #{self.class}"
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
def symbolize_foreign_key_constraint_action(constraint_action)
|
27
|
+
return nil if constraint_action.nil?
|
28
|
+
constraint_action.downcase.gsub(/\s/, '_').to_sym
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Foreign Key support from http://wiki.rubyonrails.org/rails/pages/Foreign+Key+Schema+Dumper+Plugin
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
class MysqlAdapter < AbstractAdapter
|
6
|
+
def supports_fetch_foreign_keys?
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def foreign_key_constraints(table, name = nil)
|
11
|
+
constraints = []
|
12
|
+
execute("SHOW CREATE TABLE #{table}", name).each do |row|
|
13
|
+
row[1].each do |create_line|
|
14
|
+
if create_line.strip =~ /CONSTRAINT `([^`]+)` FOREIGN KEY \(`([^`]+)`\) REFERENCES `([^`]+)` \(`([^`]+)`\)([^,]*)/
|
15
|
+
constraint = ForeignKeyConstraint.new(Regexp.last_match(1), table, Regexp.last_match(2), Regexp.last_match(3), Regexp.last_match(4), nil, nil)
|
16
|
+
|
17
|
+
constraint_params = {}
|
18
|
+
|
19
|
+
unless Regexp.last_match(5).nil?
|
20
|
+
Regexp.last_match(5).strip.split('ON ').each do |param|
|
21
|
+
constraint_params[Regexp.last_match(1).upcase] = Regexp.last_match(2).strip.upcase if param.strip =~ /([^ ]+) (.+)/
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
constraint.on_update = symbolize_foreign_key_constraint_action(constraint_params['UPDATE']) if constraint_params.include? 'UPDATE'
|
26
|
+
constraint.on_delete = symbolize_foreign_key_constraint_action(constraint_params['DELETE']) if constraint_params.include? 'DELETE'
|
27
|
+
|
28
|
+
constraints << constraint
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
constraints
|
34
|
+
end
|
35
|
+
|
36
|
+
def remove_foreign_key_constraint(table_name, constraint_name)
|
37
|
+
execute "ALTER TABLE #{table_name} DROP FOREIGN KEY #{constraint_name}"
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Foreign Key support from http://wiki.rubyonrails.org/rails/pages/Foreign+Key+Schema+Dumper+Plugin
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
class PostgreSQLAdapter < AbstractAdapter
|
6
|
+
|
7
|
+
def supports_fetch_foreign_keys?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
def foreign_key_constraints(table, name = nil)
|
12
|
+
|
13
|
+
|
14
|
+
sql = "SELECT conname, pg_catalog.pg_get_constraintdef(oid) AS consrc FROM pg_catalog.pg_constraint WHERE contype='f' "
|
15
|
+
sql += "AND conrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='#{table}')"
|
16
|
+
|
17
|
+
result = query(sql, name)
|
18
|
+
|
19
|
+
keys = []
|
20
|
+
re = /(?i)^FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)(?: ON UPDATE (\w+))?(?: ON DELETE (\w+))?$/
|
21
|
+
result.each do |row|
|
22
|
+
# pg_catalog.pg_get_constraintdef returns a string like this:
|
23
|
+
# FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE
|
24
|
+
if match = re.match(row[1])
|
25
|
+
|
26
|
+
keys << ForeignKeyConstraint.new(row[0],
|
27
|
+
table,
|
28
|
+
match[1],
|
29
|
+
match[2],
|
30
|
+
match[3],
|
31
|
+
symbolize_foreign_key_constraint_action(match[4]),
|
32
|
+
symbolize_foreign_key_constraint_action(match[5]))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
keys
|
37
|
+
end
|
38
|
+
|
39
|
+
def remove_foreign_key_constraint(table_name, constraint_name)
|
40
|
+
execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint_name}"
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,23 +1,225 @@
|
|
1
1
|
module DrNicMagicModels
|
2
|
+
|
2
3
|
class Schema
|
3
4
|
class << self
|
5
|
+
|
6
|
+
# all in lower case please
|
7
|
+
ReservedTables = [:schema_info, :sessions]
|
8
|
+
@@models = nil
|
9
|
+
|
10
|
+
def logger
|
11
|
+
@@logger ||= DrNicMagicModels::Logger
|
12
|
+
end
|
13
|
+
|
4
14
|
def models
|
5
|
-
@@models
|
6
|
-
load_schema if @@models.blank?
|
15
|
+
load_schema if @@models.nil?
|
7
16
|
@@models
|
8
17
|
end
|
9
18
|
|
10
|
-
def
|
11
|
-
@@models
|
19
|
+
def fks_on_table(table_name)
|
20
|
+
load_schema if @@models.nil?
|
21
|
+
@@fks_by_table[table_name.to_s] || []
|
12
22
|
end
|
13
23
|
|
24
|
+
# active record only support 2 column link tables, otherwise use a model table, has_many and through
|
25
|
+
def is_link_table?(table_name)
|
26
|
+
load_schema if @@models.nil?
|
27
|
+
return @@link_tables[table_name] if ! @@link_tables[table_name].nil?
|
28
|
+
column_names = @conn.columns(table_name).map{|x| x.name }
|
29
|
+
@@link_tables[table_name] = ! column_names.include?("id") && column_names.length == 2 && column_names.select { |x| x =~ /_id$/ } == column_names
|
30
|
+
return @@link_tables[table_name]
|
31
|
+
end
|
32
|
+
|
33
|
+
def link_tables_for_class(klass)
|
34
|
+
load_schema if @@models.nil?
|
35
|
+
end
|
36
|
+
|
14
37
|
def load_schema
|
15
|
-
|
16
|
-
|
17
|
-
|
38
|
+
|
39
|
+
return if ! @@models.nil?
|
40
|
+
|
41
|
+
raise "No database connection" if !(@conn = ActiveRecord::Base.connection)
|
42
|
+
|
43
|
+
@@models = ModelHash.new
|
44
|
+
@@tables = Hash.new
|
45
|
+
@@fks_by_table = Hash.new
|
46
|
+
@@link_tables = Hash.new
|
47
|
+
|
48
|
+
tables = @conn.tables.sort
|
49
|
+
|
50
|
+
# Work out which tables are in the model and which aren't
|
51
|
+
tables.each do |table_name|
|
52
|
+
|
53
|
+
# deal with reserved tables & link_tables && other stray id-less tables
|
54
|
+
next if ReservedTables.include?(table_name.downcase.to_sym) || is_link_table?(table_name) || ! @conn.columns(table_name).map{ |x| x.name}.include?("id")
|
55
|
+
|
56
|
+
# a model table then...
|
57
|
+
model_class_name = ActiveRecord::Base.class_name(table_name)
|
58
|
+
|
59
|
+
logger.debug "Got a model table: #{table_name} => class #{model_class_name}"
|
60
|
+
|
61
|
+
@@models[model_class_name] = table_name
|
62
|
+
@@tables[table_name] = model_class_name
|
63
|
+
|
64
|
+
# create by MAGIC!
|
65
|
+
begin
|
66
|
+
klass = model_class_name.constantize
|
67
|
+
rescue Exception => e
|
68
|
+
# create our class!
|
69
|
+
class_def = <<-end_eval
|
70
|
+
class #{model_class_name} < ActiveRecord::Base
|
71
|
+
set_table_name('#{table_name}')
|
72
|
+
end
|
73
|
+
end_eval
|
74
|
+
eval(class_def, TOPLEVEL_BINDING)
|
75
|
+
klass = model_class_name.constantize
|
76
|
+
end
|
77
|
+
|
78
|
+
# magic up some validation
|
79
|
+
klass.send(:extend, DrNicMagicModels::Validations)
|
80
|
+
klass.send(:generate_validations)
|
81
|
+
|
18
82
|
end
|
83
|
+
|
84
|
+
# Process FKs?
|
85
|
+
if @conn.supports_fetch_foreign_keys?
|
86
|
+
|
87
|
+
tables.each do |table_name|
|
88
|
+
logger.debug "Getting FKs for #{table_name}"
|
89
|
+
@@fks_by_table[table_name] = Array.new
|
90
|
+
@conn.foreign_key_constraints(table_name).each do |fk|
|
91
|
+
logger.debug "Got one: #{fk}"
|
92
|
+
@@fks_by_table[table_name].push(fk)
|
93
|
+
end # do each fk
|
94
|
+
|
95
|
+
end # each table
|
96
|
+
end
|
97
|
+
|
98
|
+
# Try to work out our link tables now...
|
99
|
+
@@models.keys.sort.each{|klass| process_table(@@models[klass.to_s])}
|
100
|
+
@@link_tables.keys.sort.each{|table_name| process_link_table(table_name) if @@link_tables[table_name]}
|
101
|
+
|
102
|
+
|
19
103
|
end
|
20
|
-
|
104
|
+
|
105
|
+
def process_table(table_name)
|
106
|
+
|
107
|
+
logger.debug "Processing model table #{table_name}"
|
108
|
+
|
109
|
+
# ok, so let's look at the foreign keys on the table...
|
110
|
+
belongs_to_klass = @@tables[table_name].constantize rescue return
|
111
|
+
|
112
|
+
processed_columns = Hash.new
|
113
|
+
|
114
|
+
fks_on_table(table_name).each do |fk|
|
115
|
+
logger.debug "Found FK column by suffix _id [#{fk.foreign_key}]"
|
116
|
+
has_some_klass = Inflector.classify(fk.reference_table).constantize rescue next
|
117
|
+
processed_columns[fk.foreign_key] = { :has_some_klass => has_some_klass }
|
118
|
+
processed_columns[fk.foreign_key].merge! add_has_some_belongs_to(belongs_to_klass, fk.foreign_key, has_some_klass) rescue next
|
119
|
+
end
|
120
|
+
|
121
|
+
column_names = @conn.columns(table_name).map{ |x| x.name}
|
122
|
+
column_names.each do |column_name|
|
123
|
+
next if not column_name =~ /_id$/
|
124
|
+
logger.debug "Found FK column by suffix _id [#{column_name}]"
|
125
|
+
if processed_columns.key?(column_name)
|
126
|
+
logger.debug "Skipping, already processed"
|
127
|
+
next
|
128
|
+
end
|
129
|
+
has_some_klass = Inflector.classify(column_name.sub(/_id$/,"")).constantize rescue next
|
130
|
+
processed_columns[column_name] = { :has_some_klass => has_some_klass }
|
131
|
+
processed_columns[column_name].merge! add_has_some_belongs_to(belongs_to_klass, column_name, has_some_klass) rescue next
|
132
|
+
end
|
133
|
+
|
134
|
+
#TODO: what if same classes in table?
|
135
|
+
|
136
|
+
# is this a link table with attributes? (has_many through?)
|
137
|
+
return if processed_columns.keys.length < 2
|
138
|
+
|
139
|
+
processed_columns.keys.each do |key1|
|
140
|
+
processed_columns.keys.each do |key2|
|
141
|
+
next if key1 == key2
|
142
|
+
logger.debug "\n*** #{processed_columns[key1][:has_some_class]}.send 'has_many', #{processed_columns[key2][:belongs_to_name].to_s.pluralize.to_sym}, :through => #{processed_columns[key2][:has_some_name]}\n\n"
|
143
|
+
processed_columns[key1][:has_some_class].send 'has_many', processed_columns[key2][:belongs_to_name].to_s.pluralize.to_sym, :through => processed_columns[key2][:has_some_name].to_sym
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
def add_has_some_belongs_to(belongs_to_klass, belongs_to_fk, has_some_klass)
|
151
|
+
|
152
|
+
logger.debug "Trying to add a #{belongs_to_klass} belongs_to #{has_some_klass}..."
|
153
|
+
|
154
|
+
# so this is a belongs_to & has_some style relationship...
|
155
|
+
# is it a has_many, or a has_one? Well, let's assume a has_one has a unique index on the column please... good db design, haha!
|
156
|
+
unique = belongs_to_klass.get_unique_index_columns.include?(belongs_to_fk)
|
157
|
+
belongs_to_name = belongs_to_fk.sub(/_id$/, '').to_sym
|
158
|
+
|
159
|
+
logger.debug "\n*** #{belongs_to_klass}.send 'belongs_to', #{belongs_to_name}, :class_name => #{has_some_klass}, :foreign_key => #{belongs_to_fk}\n"
|
160
|
+
belongs_to_klass.send(:belongs_to, belongs_to_name, :class_name => has_some_klass.to_s, :foreign_key => belongs_to_fk.to_sym)
|
161
|
+
|
162
|
+
# work out if we need a prefix
|
163
|
+
has_some_name = ((unique ? belongs_to_klass.table_name.singularize : belongs_to_klass.table_name.pluralize) + (belongs_to_name.to_s == has_some_klass.table_name.singularize ? "" : "_as_"+belongs_to_name.to_s)).downcase.to_sym
|
164
|
+
method = unique ? :has_one : :has_many
|
165
|
+
logger.debug "\n*** #{has_some_klass}.send(#{method}, #{has_some_name}, :class_name => #{belongs_to_klass.to_s}, :foreign_key => #{belongs_to_fk.to_sym})\n\n"
|
166
|
+
has_some_klass.send(method, has_some_name, :class_name => belongs_to_klass.to_s, :foreign_key => belongs_to_fk.to_sym)
|
167
|
+
|
168
|
+
return { :method => method, :belongs_to_name => belongs_to_name, :has_some_name => has_some_name, :has_some_class => has_some_klass }
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
def process_link_table(table_name)
|
173
|
+
|
174
|
+
logger.debug "Processing link table #{table_name}"
|
175
|
+
|
176
|
+
classes_map = Hash.new
|
177
|
+
column_names = @conn.columns(table_name).map{ |x| x.name}
|
178
|
+
|
179
|
+
# use foreign keys first
|
180
|
+
fks_on_table(table_name).each do |fk|
|
181
|
+
logger.debug "Processing fk: #{fk}"
|
182
|
+
klass = Inflector.classify(fk.reference_table).constantize rescue logger.debug("Cannot find model #{class_name} for table #{fk.reference_table}") && return
|
183
|
+
classes_map[fk.foreign_key] = klass
|
184
|
+
end
|
185
|
+
|
186
|
+
logger.debug "Got #{classes_map.keys.length} references from FKs"
|
187
|
+
|
188
|
+
if classes_map.keys.length < 2
|
189
|
+
|
190
|
+
#Fall back on good ol _id recognition
|
191
|
+
|
192
|
+
column_names.each do |column_name|
|
193
|
+
|
194
|
+
# check we haven't processed by fks already
|
195
|
+
next if ! classes_map[column_name].nil?
|
196
|
+
referenced_table = column_name.sub(/_id$/, '')
|
197
|
+
|
198
|
+
begin
|
199
|
+
klass = Inflector.classify(referenced_table).constantize
|
200
|
+
# fall back on FKs here
|
201
|
+
if ! klass.nil?
|
202
|
+
classes_map[column_name] = klass
|
203
|
+
end
|
204
|
+
rescue
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# not detected the link table?
|
210
|
+
logger.debug "Got #{classes_map.keys.length} references"
|
211
|
+
logger.debug "Cannot detect both tables referenced in link table" && return if classes_map.keys.length != 2
|
212
|
+
|
213
|
+
logger.debug "Adding habtm relationship"
|
214
|
+
|
215
|
+
logger.debug "\n*** #{classes_map[column_names[0]]}.send 'has_and_belongs_to_many', #{column_names[1].sub(/_id$/,'').pluralize.to_sym}, :class_name => #{classes_map[column_names[1]].to_s}, :join_table => #{table_name.to_sym}\n"
|
216
|
+
logger.debug "\n*** #{classes_map[column_names[1]]}.send 'has_and_belongs_to_many', #{column_names[0].sub(/_id$/,'').pluralize.to_sym}, :class_name => #{classes_map[column_names[0]].to_s}, :join_table => #{table_name.to_sym}\n\n"
|
217
|
+
|
218
|
+
classes_map[column_names[0]].send 'has_and_belongs_to_many', column_names[1].sub(/_id$/,'').pluralize.to_sym, :class_name => classes_map[column_names[1]].to_s, :join_table => table_name.to_sym
|
219
|
+
classes_map[column_names[1]].send 'has_and_belongs_to_many', column_names[0].sub(/_id$/,'').pluralize.to_sym, :class_name => classes_map[column_names[0]].to_s, :join_table => table_name.to_sym
|
220
|
+
|
221
|
+
end
|
222
|
+
end
|
21
223
|
end
|
22
224
|
|
23
225
|
class ModelHash < Hash
|
@@ -1,26 +1,43 @@
|
|
1
1
|
module DrNicMagicModels
|
2
2
|
module Validations
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
|
4
|
+
def generate_validations
|
5
|
+
|
6
|
+
logger = DrNicMagicModels::Logger
|
7
|
+
|
8
|
+
# Code reworked from http://www.redhillconsulting.com.au/rails_plugins.html
|
9
|
+
# Thanks Red Hill Consulting for using an MIT licence :o)
|
10
|
+
|
11
|
+
# NOT NULL constraints
|
12
|
+
self.columns.reject { |column| column.name =~ /(?i)^(((created|updated)_(at|on))|position|type|id)$/ }.each do |column|
|
7
13
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
add_validation :validates_presence_of, column_names
|
14
|
+
if column.type == :integer
|
15
|
+
logger.debug "validates_numericality_of #{column.name}, :allow_nil => true, :only_integer => true"
|
16
|
+
self.validates_numericality_of column.name, :allow_nil => true, :only_integer => true
|
17
|
+
elsif column.number?
|
18
|
+
logger.debug "validates_numericality_of #{column.name}, :allow_nil => true"
|
19
|
+
self.validates_numericality_of column.name, :allow_nil => true
|
20
|
+
elsif column.text? && column.limit
|
21
|
+
logger.debug "validates_length_of #{column.name}, :allow_nil => true, :maximum => #{column.limit}"
|
22
|
+
self.validates_length_of column.name, :allow_nil => true, :maximum => column.limit
|
18
23
|
end
|
19
|
-
end
|
20
24
|
|
21
|
-
|
22
|
-
|
25
|
+
# Active record seems to interpolate booleans anyway to either true, false or nil...
|
26
|
+
if column.type == :boolean
|
27
|
+
logger.debug "validates_inclusion_of #{column.name}, :in => [true, false], :allow_nil => #{column.null}, :message => ActiveRecord::Errors.default_error_messages[:blank]"
|
28
|
+
self.validates_inclusion_of column.name, :in => [true, false], :allow_nil => column.null, :message => ActiveRecord::Errors.default_error_messages[:blank]
|
29
|
+
elsif !column.null
|
30
|
+
logger.debug "validates_presence_of #{column.name}"
|
31
|
+
self.validates_presence_of column.name
|
32
|
+
end
|
23
33
|
end
|
24
|
-
|
34
|
+
|
35
|
+
# Single-column UNIQUE indexes
|
36
|
+
get_unique_index_columns.each do |col|
|
37
|
+
logger.debug "validates_uniqueness_of #{col}"
|
38
|
+
self.validates_uniqueness_of col
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
25
42
|
end
|
26
43
|
end
|
data/lib/dr_nic_magic_models.rb
CHANGED
@@ -3,7 +3,6 @@ $:.unshift(File.dirname(__FILE__)) unless
|
|
3
3
|
|
4
4
|
unless defined?(ActiveRecord)
|
5
5
|
begin
|
6
|
-
$:.unshift(File.dirname(__FILE__) + "/../../activerecord/lib")
|
7
6
|
require 'active_record'
|
8
7
|
rescue LoadError
|
9
8
|
require 'rubygems'
|
@@ -11,7 +10,19 @@ unless defined?(ActiveRecord)
|
|
11
10
|
end
|
12
11
|
end
|
13
12
|
|
13
|
+
module DrNicMagicModels
|
14
|
+
Logger = RAILS_DEFAULT_LOGGER rescue Logger.new(STDERR)
|
15
|
+
end
|
16
|
+
|
14
17
|
require 'dr_nic_magic_models/schema'
|
15
18
|
require 'dr_nic_magic_models/validations'
|
16
|
-
require 'module'
|
17
19
|
require 'base'
|
20
|
+
require 'rails' rescue nil
|
21
|
+
require 'connection_adapters/abstract_adapter'
|
22
|
+
require 'connection_adapters/mysql_adapter'
|
23
|
+
require 'connection_adapters/postgresql_adapter'
|
24
|
+
|
25
|
+
# load the schema
|
26
|
+
DrNicMagicModels::Schema.load_schema
|
27
|
+
|
28
|
+
|
data/lib/module.rb
CHANGED
@@ -1,18 +0,0 @@
|
|
1
|
-
class Module
|
2
|
-
alias :normal_const_missing :const_missing
|
3
|
-
|
4
|
-
def const_missing(class_id)
|
5
|
-
begin
|
6
|
-
return normal_const_missing(class_id)
|
7
|
-
rescue
|
8
|
-
end
|
9
|
-
unless table_name = DrNicMagicModels::Schema.models[class_id]
|
10
|
-
raise NameError.new("uninitialized constant #{class_id}") if DrNicMagicModels::Schema.models.enquired? class_id
|
11
|
-
end
|
12
|
-
klass_code = lambda {klass_code}
|
13
|
-
klass = Class.new ActiveRecord::Base, &klass_code
|
14
|
-
const_set class_id, klass
|
15
|
-
klass.set_table_name table_name
|
16
|
-
klass
|
17
|
-
end
|
18
|
-
end
|
data/lib/rails.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dependencies #:nodoc:#
|
2
|
+
|
3
|
+
@@models_dir = File.expand_path(File.join(RAILS_ROOT,'app','models'))
|
4
|
+
|
5
|
+
# don't reload models... it doesn't work anyway, not sure why they haven't done this?
|
6
|
+
# submit as patch?
|
7
|
+
alias require_or_load_old require_or_load
|
8
|
+
def require_or_load(file_name, *args)
|
9
|
+
file_name = $1 if file_name =~ /^(.*)\.rb$/
|
10
|
+
expanded = File.expand_path(file_name)
|
11
|
+
old_mechanism = mechanism
|
12
|
+
if expanded =~ /^#{@@models_dir}/
|
13
|
+
RAILS_DEFAULT_LOGGER.debug "*** Not reloading #{file_name}"
|
14
|
+
Dependencies.mechanism = :require
|
15
|
+
end
|
16
|
+
require_or_load_old(file_name, *args)
|
17
|
+
mechanism = old_mechanism
|
18
|
+
end
|
19
|
+
end
|
File without changes
|