dr_nic_magic_models 0.7.1 → 0.8.0
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.
- 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
|