Initial import

This commit is contained in:
2008-03-02 16:04:34 -03:00
commit 5e4951fa47
798 changed files with 59730 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
#TODO: Use :dependent for FK cascade?
module ActiveRecord
class Base
class << self
public
def get_unique_index_columns
self.connection.indexes(self.table_name, "#{self.name} Indexes").select { |index| index.unique && index.columns.size == 1 }.map{ |index| index.columns.first }
end
end
end
end

View File

@@ -0,0 +1,32 @@
module ActiveRecord
module ConnectionAdapters # :nodoc:
# Generic holder for foreign key constraint meta-data from the database schema.
class ForeignKeyConstraint < Struct.new(:name, :table, :foreign_key, :reference_table, :reference_column, :on_update, :on_delete)
end
class AbstractAdapter
# Does this adapter support the ability to fetch foreign key information?
# Backend specific, as the abstract adapter always returns +false+.
def supports_fetch_foreign_keys?
false
end
def foreign_key_constraints(table, name = nil)
raise NotImplementedError, "foreign_key_constraints is not implemented for #{self.class}"
end
def remove_foreign_key_constraint(table_name, constraint_name)
raise NotImplementedError, "rename_table is not implemented for #{self.class}"
end
protected
def symbolize_foreign_key_constraint_action(constraint_action)
return nil if constraint_action.nil?
constraint_action.downcase.gsub(/\s/, '_').to_sym
end
end
end
end

View File

@@ -0,0 +1,42 @@
# Foreign Key support from http://wiki.rubyonrails.org/rails/pages/Foreign+Key+Schema+Dumper+Plugin
module ActiveRecord
module ConnectionAdapters
class MysqlAdapter < AbstractAdapter
def supports_fetch_foreign_keys?
true
end
def foreign_key_constraints(table, name = nil)
constraints = []
execute("SHOW CREATE TABLE #{table}", name).each do |row|
row[1].each do |create_line|
if create_line.strip =~ /CONSTRAINT `([^`]+)` FOREIGN KEY \(`([^`]+)`\) REFERENCES `([^`]+)` \(`([^`]+)`\)([^,]*)/
constraint = ForeignKeyConstraint.new(Regexp.last_match(1), table, Regexp.last_match(2), Regexp.last_match(3), Regexp.last_match(4), nil, nil)
constraint_params = {}
unless Regexp.last_match(5).nil?
Regexp.last_match(5).strip.split('ON ').each do |param|
constraint_params[Regexp.last_match(1).upcase] = Regexp.last_match(2).strip.upcase if param.strip =~ /([^ ]+) (.+)/
end
end
constraint.on_update = symbolize_foreign_key_constraint_action(constraint_params['UPDATE']) if constraint_params.include? 'UPDATE'
constraint.on_delete = symbolize_foreign_key_constraint_action(constraint_params['DELETE']) if constraint_params.include? 'DELETE'
constraints << constraint
end
end
end
constraints
end
def remove_foreign_key_constraint(table_name, constraint_name)
execute "ALTER TABLE #{table_name} DROP FOREIGN KEY #{constraint_name}"
end
end
end
end

View File

@@ -0,0 +1,45 @@
# Foreign Key support from http://wiki.rubyonrails.org/rails/pages/Foreign+Key+Schema+Dumper+Plugin
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter < AbstractAdapter
def supports_fetch_foreign_keys?
true
end
def foreign_key_constraints(table, name = nil)
sql = "SELECT conname, pg_catalog.pg_get_constraintdef(oid) AS consrc FROM pg_catalog.pg_constraint WHERE contype='f' "
sql += "AND conrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='#{table}')"
result = query(sql, name)
keys = []
re = /(?i)^FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)(?: ON UPDATE (\w+))?(?: ON DELETE (\w+))?$/
result.each do |row|
# pg_catalog.pg_get_constraintdef returns a string like this:
# FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE
if match = re.match(row[1])
keys << ForeignKeyConstraint.new(row[0],
table,
match[1],
match[2],
match[3],
symbolize_foreign_key_constraint_action(match[4]),
symbolize_foreign_key_constraint_action(match[5]))
end
end
keys
end
def remove_foreign_key_constraint(table_name, constraint_name)
execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint_name}"
end
end
end
end

View File

@@ -0,0 +1,34 @@
$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
unless defined?(ActiveRecord)
begin
require 'active_record'
rescue LoadError
require 'rubygems'
require_gem 'activerecord'
end
end
module DrNicMagicModels
Logger = RAILS_DEFAULT_LOGGER rescue Logger.new(STDERR)
end
require 'dr_nic_magic_models/magic_model'
require 'dr_nic_magic_models/schema'
require 'dr_nic_magic_models/validations'
require 'dr_nic_magic_models/inflector'
require 'base'
require 'module'
require 'rails' rescue nil
require 'connection_adapters/abstract_adapter'
require 'connection_adapters/mysql_adapter'
require 'connection_adapters/postgresql_adapter'
# load the schema
# TODO - add this to README - DrNicMagicModels::Schema.load_schema(true)
class ActiveRecord::Base
include DrNicMagicModels::MagicModel
extend DrNicMagicModels::Validations
end

View File

@@ -0,0 +1,14 @@
module DrNicMagicModels
class Inflector
def table_names ; DrNicMagicModels::Schema.table_names; end
def tables ; DrNicMagicModels::Schema.tables; end
def models ; DrNicMagicModels::Schema.model; end
def class_name(table_name)
ActiveRecord::Base.class_name(table_name)
end
def post_class_creation(klass)
end
end
end

View File

@@ -0,0 +1,133 @@
# Mixed into a class that is dynamically created, unless
# the class was created by the Schema.load_schema process
# which builds the whole class, thus no magicalness is
# needed
module DrNicMagicModels::MagicModel
def self.append_features(base)
super
base.send(:include, InstanceMethods)
class << base
# Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
def reflect_on_association(association)
unless reflections[association]
# See if an assocation can be generated
self.new.send(association) rescue nil
end
reflections[association].is_a?(ActiveRecord::Reflection::AssociationReflection) ? reflections[association] : nil
end
end
end
module InstanceMethods
def method_missing(method, *args, &block)
begin
super
rescue
if unknown_method? method
result = find_belongs_to method, *args, &block
result = find_has_some method, *args, &block if not result
result = find_has_some_indirect method, *args, &block if not result
return result if result
end
add_known_unknown method
raise
end
end
def add_known_unknown(method)
@known_unknowns ||= {}
@known_unknowns[method] = true
end
def unknown_method?(method)
@known_unknowns.nil? or @known_unknowns.include? method
end
def find_belongs_to(method, *args, &block)
method_clean = clean_method method
fkc =
begin
self.class.connection.foreign_key_constraints(self.class.table_name, method_clean)
rescue NotImplementedError
nil
end
if !fkc.nil? && fkc.length > 0
foreign_key = fkc.first.foreign_key
options = {:dependent => :destroy,
:foreign_key => fkc.first.foreign_key,
:class_name => self.class.class_name(fkc.first.reference_table)}
else
foreign_key = self.class.columns.select {|column| column.name == method_clean.to_s.foreign_key}.first
end
options ||= {}
return add_belongs_to(method, method_clean, options, *args, &block) if foreign_key
end
def add_belongs_to(method, method_clean, options, *args, &block)
self.class.send 'belongs_to', method_clean.to_sym, options rescue puts $!
self.send(method, *args, &block)
end
def find_has_some(method, *args, &block)
method_clean = clean_method method
fkc = [method_clean.to_s.pluralize, method_clean.to_s.singularize].inject({}) do |pair, table_name|
fkc = begin
self.class.connection.foreign_key_constraints(table_name)
rescue NotImplementedError
nil
end
pair[table_name] = fkc if not fkc.blank?
pair
end
if not fkc.blank?
# assumes there is only one table found - that schema doesn't have a singular and plural table of same name
foreign_key = fkc.values.first.find {|fk| fk.reference_table == self.class.table_name}
if foreign_key
foreign_key = foreign_key.foreign_key
table_name = fkc.keys.first
klass = Module.const_get table_name.singularize.camelize rescue nil
options = {:foreign_key => foreign_key, :class_name => klass.name}
end
end
unless foreign_key
klass = Module.const_get method_clean.to_s.downcase.singularize.camelize rescue nil
foreign_key = klass.columns.select {|column| column.name == self.class.name.foreign_key}.first if klass
end
options ||= {}
return add_has_some(method, method_clean, options, *args, &block) if foreign_key
end
def add_has_some(method, method_clean, options, *args, &block)
association = method_clean.singularize == method_clean ? 'has_one' : 'has_many'
self.class.send association, method_clean.to_sym, options rescue puts $!
self.send(method, *args, &block)
end
def find_has_some_indirect(method, *args, &block)
klass = Module.const_get method.to_s.downcase.singularize.camelize rescue return
join_table = nil
self.connection.tables.each do |table|
unless [self.class.table_name, klass.table_name].include? table
columns = self.connection.columns(table).map(&:name)
join_table = table if columns.include?(self.class.to_s.foreign_key) and columns.include?(klass.to_s.foreign_key)
end
break if join_table
end
return add_has_some_through(join_table, method, *args, &block) if join_table
end
def add_has_some_through(join_table, method, *args, &block)
self.class.send 'has_many', method, :through => join_table.to_sym
self.send(method, *args, &block)
end
private
def clean_method(method)
method.to_s.gsub(/=$/,'') # remove any = from the end of the method name
end
end
end

View File

@@ -0,0 +1,270 @@
module DrNicMagicModels
# ONE Schema per namespace module
# Person, Company, etc share the Object namespace module, ie. ::Person, ::Company
# Blog::Post, Blog::Comment, share the Blog namespace module
class Schema
attr_reader :modul
def initialize(modul)
@modul = modul
@table_name_prefix = modul.instance_variable_get("@table_name_prefix") rescue ''
logger.info "Create Schema for #{@modul}, table_name_prefix '#{@table_name_prefix}'"
end
cattr_accessor :inflector
cattr_accessor :superklass
# Need to store models etc per-module, not in @ @models
def inflector
@inflector ||= Inflector.new
end
# all in lower case please
ReservedTables = [:schema_info, :sessions]
@models = nil
def logger
@logger ||= DrNicMagicModels::Logger
end
def models
load_schema if @models.nil?
@models
end
def tables
load_schema if @tables.nil?
@tables
end
def table_names
load_schema if @table_names.nil?
@table_names
end
def fks_on_table(table_name)
load_schema if @models.nil?
@fks_by_table[table_name.to_s] || []
end
# active record only support 2 column link tables, otherwise use a model table, has_many and through
def is_link_table?(table_name)
load_schema if @models.nil?
return @link_tables[table_name] if ! @link_tables[table_name].nil?
column_names = @conn.columns(table_name).map{|x| x.name }
@link_tables[table_name] = ! column_names.include?("id") && column_names.length == 2 && column_names.select { |x| x =~ /_id$/ } == column_names
return @link_tables[table_name]
end
def link_tables_for_class(klass)
load_schema if @models.nil?
end
def load_schema(preload = false)
return if !@models.nil?
@superklass ||= ActiveRecord::Base
raise "No database connection" if !(@conn = @superklass.connection)
@models = ModelHash.new
@tables = Hash.new
@fks_by_table = Hash.new
@link_tables = Hash.new
@table_names = @conn.tables
@table_names = @table_names.grep(/^#{@table_name_prefix}/) if @table_name_prefix
@table_names = @table_names.sort
logger.info "For #{modul} tables are #{@table_names.inspect}"
# Work out which tables are in the model and which aren't
@table_names.each do |table_name|
# deal with reserved tables & link_tables && other stray id-less tables
#key = 'id'
#case ActiveRecord::Base.primary_key_prefix_type
# when :table_name
# key = Inflector.foreign_key(table_name, false)
# when :table_name_with_underscore
# key = Inflector.foreign_key(table_name)
#end
#next if ReservedTables.include?(table_name.downcase.to_sym) ||
# is_link_table?(table_name) ||
# ! @conn.columns(table_name).map{ |x| x.name}.include?(key)
table_name_clean = table_name.gsub(/^#{@table_name_prefix}/,'')
# a model table then...
model_class_name = inflector.class_name(table_name_clean)
logger.debug "Got a model table: #{table_name} => class #{model_class_name}"
@models[model_class_name] = table_name
@tables[table_name] = model_class_name
if preload
# create by MAGIC!
klass = model_class_name.constantize
# Process FKs?
if @conn.supports_fetch_foreign_keys?
tables.each do |table_name|
logger.debug "Getting FKs for #{table_name}"
@fks_by_table[table_name] = Array.new
@conn.foreign_key_constraints(table_name).each do |fk|
logger.debug "Got one: #{fk}"
@fks_by_table[table_name].push(fk)
end # do each fk
end # each table
end
# Try to work out our link tables now...
@models.keys.sort.each{|klass| process_table(@models[klass.to_s])}
@link_tables.keys.sort.each{|table_name| process_link_table(table_name) if @link_tables[table_name]}
end
end
end
def process_table(table_name)
logger.debug "Processing model table #{table_name}"
# ok, so let's look at the foreign keys on the table...
belongs_to_klass = @tables[table_name].constantize rescue return
processed_columns = Hash.new
fks_on_table(table_name).each do |fk|
logger.debug "Found FK column by suffix _id [#{fk.foreign_key}]"
has_some_klass = Inflector.classify(fk.reference_table).constantize rescue next
processed_columns[fk.foreign_key] = { :has_some_klass => has_some_klass }
processed_columns[fk.foreign_key].merge! add_has_some_belongs_to(belongs_to_klass, fk.foreign_key, has_some_klass) rescue next
end
column_names = @conn.columns(table_name).map{ |x| x.name}
column_names.each do |column_name|
next if not column_name =~ /_id$/
logger.debug "Found FK column by suffix _id [#{column_name}]"
if processed_columns.key?(column_name)
logger.debug "Skipping, already processed"
next
end
has_some_klass = Inflector.classify(column_name.sub(/_id$/,"")).constantize rescue next
processed_columns[column_name] = { :has_some_klass => has_some_klass }
processed_columns[column_name].merge! add_has_some_belongs_to(belongs_to_klass, column_name, has_some_klass) rescue next
end
#TODO: what if same classes in table?
# is this a link table with attributes? (has_many through?)
return if processed_columns.keys.length < 2
processed_columns.keys.each do |key1|
processed_columns.keys.each do |key2|
next if key1 == key2
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"
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
end
end
end
def add_has_some_belongs_to(belongs_to_klass, belongs_to_fk, has_some_klass)
logger.debug "Trying to add a #{belongs_to_klass} belongs_to #{has_some_klass}..."
# so this is a belongs_to & has_some style relationship...
# 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!
unique = belongs_to_klass.get_unique_index_columns.include?(belongs_to_fk)
belongs_to_name = belongs_to_fk.sub(/_id$/, '').to_sym
logger.debug "\n*** #{belongs_to_klass}.send 'belongs_to', #{belongs_to_name}, :class_name => #{has_some_klass}, :foreign_key => #{belongs_to_fk}\n"
belongs_to_klass.send(:belongs_to, belongs_to_name, :class_name => has_some_klass.to_s, :foreign_key => belongs_to_fk.to_sym)
# work out if we need a prefix
has_some_name = (
(unique ? belongs_to_klass.table_name.singularize : belongs_to_klass.table_name) +
(belongs_to_name.to_s == has_some_klass.table_name.singularize ? "" : "_as_"+belongs_to_name.to_s)
).downcase.to_sym
method = unique ? :has_one : :has_many
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"
has_some_klass.send(method, has_some_name, :class_name => belongs_to_klass.to_s, :foreign_key => belongs_to_fk.to_sym)
return { :method => method, :belongs_to_name => belongs_to_name, :has_some_name => has_some_name, :has_some_class => has_some_klass }
end
def process_link_table(table_name)
logger.debug "Processing link table #{table_name}"
classes_map = Hash.new
column_names = @conn.columns(table_name).map{ |x| x.name}
# use foreign keys first
fks_on_table(table_name).each do |fk|
logger.debug "Processing fk: #{fk}"
klass = Inflector.classify(fk.reference_table).constantize rescue logger.debug("Cannot find model #{class_name} for table #{fk.reference_table}") && return
classes_map[fk.foreign_key] = klass
end
logger.debug "Got #{classes_map.keys.length} references from FKs"
if classes_map.keys.length < 2
#Fall back on good ol _id recognition
column_names.each do |column_name|
# check we haven't processed by fks already
next if ! classes_map[column_name].nil?
referenced_table = column_name.sub(/_id$/, '')
begin
klass = Inflector.classify(referenced_table).constantize
# fall back on FKs here
if ! klass.nil?
classes_map[column_name] = klass
end
rescue
end
end
end
# not detected the link table?
logger.debug "Got #{classes_map.keys.length} references"
logger.debug "Cannot detect both tables referenced in link table" && return if classes_map.keys.length != 2
logger.debug "Adding habtm relationship"
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"
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"
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
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
end
end
class ModelHash < Hash
def unenquire(class_id)
@enquired ||= {}
@enquired[class_id = class_id.to_s] = false
end
def enquired?(class_id)
@enquired ||= {}
@enquired[class_id.to_s]
end
def [](class_id)
enquired?(class_id = class_id.to_s)
@enquired[class_id] = true
super(class_id)
end
end
end

View File

@@ -0,0 +1,46 @@
module DrNicMagicModels
module Validations
def generate_validations
logger = DrNicMagicModels::Logger
# Ensure that the connection to db is established, else validations don't get created.
ActiveRecord::Base.connection
# Code reworked from http://www.redhillconsulting.com.au/rails_plugins.html
# Thanks Red Hill Consulting for using an MIT licence :o)
# NOT NULL constraints
self.columns.reject { |column| column.name =~ /(?i)^(((created|updated)_(at|on))|position|type|id)$/ }.each do |column|
if column.type == :integer
logger.debug "validates_numericality_of #{column.name}, :allow_nil => #{column.null.inspect}, :only_integer => true"
self.validates_numericality_of column.name, :allow_nil => column.null, :only_integer => true
elsif column.number?
logger.debug "validates_numericality_of #{column.name}, :allow_nil => #{column.null.inspect}"
self.validates_numericality_of column.name, :allow_nil => column.null
elsif column.text? && column.limit
logger.debug "validates_length_of #{column.name}, :allow_nil => #{column.null.inspect}, :maximum => #{column.limit}"
self.validates_length_of column.name, :allow_nil => column.null, :maximum => column.limit
end
# Active record seems to interpolate booleans anyway to either true, false or nil...
if column.type == :boolean
logger.debug "validates_inclusion_of #{column.name}, :in => [true, false], :allow_nil => #{column.null}, :message => ActiveRecord::Errors.default_error_messages[:blank]"
self.validates_inclusion_of column.name, :in => [true, false], :allow_nil => column.null, :message => ActiveRecord::Errors.default_error_messages[:blank]
elsif !column.null
logger.debug "validates_presence_of #{column.name}"
self.validates_presence_of column.name
end
end
# Single-column UNIQUE indexes
get_unique_index_columns.each do |col|
logger.debug "validates_uniqueness_of #{col}"
self.validates_uniqueness_of col
end
end
end
end

View File

@@ -0,0 +1,9 @@
module DrNicMagicModels #:nodoc:
module VERSION #:nodoc:
MAJOR = 0
MINOR = 9
TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end
end

View File

@@ -0,0 +1,33 @@
class Module
alias :normal_const_missing :const_missing
def const_missing(class_id)
begin
return normal_const_missing(class_id)
rescue
end
@magic_schema ||= DrNicMagicModels::Schema.new self
unless table_name = @magic_schema.models[class_id]
raise NameError.new("uninitialized constant #{class_id}") if @magic_schema.models.enquired? class_id
end
superklass = @magic_schema.superklass || ActiveRecord::Base
klass = create_class(class_id, superklass) do
set_table_name table_name
# include DrNicMagicModels::MagicModel
# extend DrNicMagicModels::Validations
end
klass.generate_validations # need to call this AFTER the class name has been assigned
@magic_schema.inflector.post_class_creation klass
klass
end
def magic_module(options)
self.instance_variable_set "@table_name_prefix", options[:table_name_prefix] if options[:table_name_prefix]
end
private
def create_class(class_name, superclass, &block)
klass = Class.new superclass, &block
self.const_set class_name, klass
end
end

View File

@@ -0,0 +1,19 @@
module Dependencies #:nodoc:#
@@models_dir = File.expand_path(File.join(RAILS_ROOT,'app','models'))
# don't reload models... it doesn't work anyway, not sure why they haven't done this?
# submit as patch?
alias require_or_load_old require_or_load
def require_or_load(file_name, *args)
file_name = $1 if file_name =~ /^(.*)\.rb$/
expanded = File.expand_path(file_name)
old_mechanism = Dependencies.mechanism
if expanded =~ /^#{@@models_dir}/
RAILS_DEFAULT_LOGGER.debug "*** Not reloading #{file_name}"
Dependencies.mechanism = :require
end
require_or_load_old(file_name, *args)
Dependencies.mechanism = old_mechanism
end
end