drysql 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,119 @@
1
+ ====================================================================
2
+ NOTE: Dave Thomas' book Programming Ruby (Pragmatic Programmers' Guide) was my
3
+ bible during the development of this plug-in so I may have inadvertently borrowed some
4
+ code from the book, and I most certainly borrowed ideas and coding conventions from the book.
5
+ Thanks for writing such a great book Dave!
6
+ ====================================================================
7
+
8
+ DrySQL v.0.1.0
9
+ ==========
10
+
11
+ The idea behind DrySQL is that if you have defined a schema on your database complete with keys, constraints,
12
+ relationships, and behaviour, then you shouldn't need to re-define any of this in your application code.
13
+
14
+ DrySQL retrieves metadata from your DB's information schema, and uses this to identify keys, and generate
15
+ associations and validations for your ActiveRecord::Base subclasses
16
+
17
+ For an overview of the features and some use cases, please visit the DrySQL project website:
18
+ http://drysql.rubyforge.org
19
+
20
+ How Do I Install It?
21
+ ============
22
+
23
+ Assuming that you have installed RubyGems, on the command line type the following:
24
+
25
+ gem install drysql
26
+
27
+ That's it!
28
+
29
+ How Does It Work?
30
+ ============
31
+
32
+ 1. Loading the DrySQL gem
33
+ -------------------------------
34
+ DrySQL simply extends ActiveRecord, so all you need to do is insert the following statement in your code
35
+ somewhere where it will be processed before your application needs to use DrySQL's behaviour:
36
+
37
+ require_gem 'drysql'
38
+
39
+ Note that DrySQL depends on the ActiveRecord gem, which in turn depends on the ActiveSupport gem, so you will
40
+ need to have these installed as well.
41
+
42
+ 2. Mapping classes to tables
43
+ --------------------------------
44
+
45
+ DrySQL makes the assumption that your table name and class name will be compatible with Rails naming
46
+ conventions, or you will specify the table name explicitly using set_table_name inside your class definition.
47
+
48
+ If your DB tables follow the Rails naming conventions, then you don't need to define classes for them at all.
49
+ The first time you reference a non-existant class, DrySQL will generate the class for you.
50
+
51
+ eg.
52
+ The first time you reference the non-existant class Employee in your Ruby code, DrySQL will generate the class
53
+ ActiveRecord::Base::Employee in memory. If your DB table is called Employees, then Rails will map the Employee
54
+ class to the Employees table once you instantiate an Employee object.
55
+
56
+ If you feel more comfortable defining your class explicitly, it can be as simple as this:
57
+
58
+ class Employee < ActiveRecord::Base
59
+ end
60
+
61
+
62
+ If your Employee table is not called Employees, then you will need to define your class in your Ruby application,
63
+ and call set_table_name inside your class definition:
64
+
65
+ class Employee < ActiveRecord::Base
66
+ set_table_name "XYZ123"
67
+ end
68
+
69
+
70
+ 3. Generating Keys, Associations, and Validations
71
+ --------------------------------------------------------
72
+
73
+ The first time you instantiate an ActiveRecord::Base subclass (we'll continue to use Employee for these examples),
74
+ DrySQL will intercept the instantiation, query the DB's information schema for the constraints and column metadata
75
+ associated with your table, and use this information to identify the keys on the table, and generate associations and
76
+ validations for the class.
77
+
78
+ Rather than assume that your primary key is called table_name_id, DrySQL queries the DB to find out exactly what your
79
+ primary key column is called. The same is true of foreign keys. Thus, DrySQL fully supports your legacy DB tables.
80
+
81
+ DrySQL can currently generate the following associations:
82
+ belongs_to
83
+ has_one
84
+ has_many
85
+ has_many :through
86
+
87
+ Supposing that I find a bulletproof way to identify has_and_belongs_to_many associations from the information schema,
88
+ I will add functionality for auto-generating these associations.
89
+
90
+ DrySQL can currently generate the following validations:
91
+ validates_length_of
92
+ validates_numericality_of
93
+ validates_inclusion_of (for boolean columns only, CHECK constraints can be used to enhance this)
94
+ validates_nullability_of
95
+
96
+ validates_nullability_of is a new validation introduced in DrySQL. Common convention is to use validates_presence_of
97
+ on any column with a NOT NULL constraint. There are 2 holes in this approach:
98
+ 1) For character columns this validation will throw an error if the value of the column is the empty string, even though the DB considers this a perfectly valid value
99
+ 2) For columns that either have a default value specified or are auto-generated by the DB, we do not want to throw an error if the value is null
100
+
101
+ validates_nullability_of throws an error only if the value is nil, no default value is specified for the column, and the column value is not auto-generated by the DB
102
+
103
+ DrySQL, by design, does not auto-generate validates_uniqueness_of, because I am not convinced about its usefulness.
104
+ Validating records before querying the DB is useful because we can identify invalid data and avoid the cost of I/O by attempting to
105
+ insert/update invalid data into the DB. validates_uniquess_of needs to query the DB in order to perform the validation, so I fail to see the advantage
106
+ of using this application-side validation over just letting the DB perform the validation and having your app handle the duplicate key error thrown
107
+ by the DB.
108
+
109
+
110
+ 4. A Note About the Unit Tests
111
+ -----------------------------------
112
+
113
+ I did not publish the tests with the first release of DrySQL because they are built on a clone of a corporate database, and I need to obfuscate them first.
114
+ I will publish tests with the next release
115
+
116
+ Enjoy!
117
+
118
+
119
+
@@ -0,0 +1,90 @@
1
+ module ActiveRecord
2
+
3
+ class Base
4
+
5
+ class << self
6
+
7
+ # The @associations instvar is used as an indicator as to whether the associations have already been generated or not.
8
+ # The first time an instance of a DrySQL-enabled subclass of ActiveRecord::Base is created, the associations are generated
9
+ # for the class.
10
+ def generate_associations
11
+ unless @associations
12
+ generate_belongs_to_associations
13
+ generate_has_many_associations
14
+ generate_through_associations
15
+ @associations = reflections
16
+ end
17
+ end
18
+
19
+ def is_associated_with(class_name_symbol)
20
+ !(reflections.detect {|key, reflection| key.to_s.singularize == class_name_symbol.to_s.singularize}).nil?
21
+ end
22
+
23
+ private
24
+
25
+ # A belongs_to association exists from class A to class B if A contains a foreign key into B
26
+ def generate_belongs_to_associations
27
+ foreign_keys = table_constraints.select {|constraint| constraint.foreign_key?}
28
+ foreign_keys.each do |foreign_key|
29
+ belongs_to_class_name = (class_name(foreign_key.referenced_table_name.upcase)).downcaseFirstLetter
30
+ self.send(:belongs_to, :"#{belongs_to_class_name}", :foreign_key=>foreign_key.column_name)
31
+ logger.info("GENERATED ASSOCIATION: #{self.name} belongs_to :#{belongs_to_class_name}, :foreign_key=>#{foreign_key.column_name}")
32
+ end
33
+ end
34
+
35
+ # A has_many association exists from A to B if B contains a foreign key into A.
36
+ # If B's foreign key into A is a unique key within B, then the association is A has_one B
37
+ def generate_has_many_associations
38
+ foreign_constraints.each do |foreign_constraint|
39
+ if foreign_constraint_column_is_unique?(foreign_constraint)
40
+ has_one_class_name = (class_name(foreign_constraint.table_name.upcase)).downcaseFirstLetter
41
+ self.send(:has_one, :"#{has_one_class_name}", :foreign_key=>foreign_constraint.referenced_column_name)
42
+ logger.info("GENERATED ASSOCIATION: #{self.name} has_one :#{has_one_class_name}, :foreign_key=>#{foreign_constraint.referenced_column_name}")
43
+ else
44
+ has_many_class_name = (class_name(foreign_constraint.table_name.upcase)).downcaseFirstLetter.pluralize
45
+ self.send(:has_many, :"#{has_many_class_name}", :foreign_key=>foreign_constraint.referenced_column_name)
46
+ logger.info("GENERATED ASSOCIATION: #{self.name} has_many :#{has_many_class_name}, :foreign_key=>#{foreign_constraint.referenced_column_name}")
47
+ end
48
+ end
49
+ end
50
+
51
+ def foreign_constraint_column_is_unique?(constraint)
52
+ class_name = class_name(constraint.table_name.upcase)
53
+ klass = instance_eval(class_name)
54
+ constraints_on_given_column = klass.table_constraints.select {|current| current.column_name == constraint.column_name}
55
+ constraints_on_given_column.any? {|current| current.unique_key?}
56
+ end
57
+
58
+ # Identifying has_many :through associations:
59
+ # 1) Collect all has_many associations for this class
60
+ #
61
+ # 2) Collect all FKs from each has_many class that do not reference this class's table
62
+ #
63
+ # 3) Create a has_many :through association for each class referenced by the FKs from step 2,
64
+ # unless this class already has an association to the class referenced by the FK
65
+ #
66
+ # i.e. if this class already has a has_many association with B, don't create a has_many B :through C
67
+ # because this will overwrite our direct association
68
+ #
69
+ # FIXME: this logic doesn't stand up when A has_many :B, :through=>C *and* A has_many :B, :through=>D
70
+ # Each association will atttempt to create an accessor for B on A, the next overwriting the previous.
71
+ # --> I'm not sure that there is any way to fix this programmatically
72
+ def generate_through_associations
73
+ has_many_associations.each do |association|
74
+ # FIXME the singularization is a hack. We should be able to figure the exact class name without relying
75
+ # on naming conventions
76
+ foreign_class = instance_eval(association[0].to_s.camelize.singularize)
77
+ eligible_foreign_keys = foreign_class.table_constraints.select {|c| c.foreign_key? && c.referenced_table_name.upcase != table_name.upcase}
78
+ eligible_foreign_keys.select {|key| !self.is_associated_with(symbolized_class_name_from_table_name(key.referenced_table_name))}.each do |fk|
79
+ has_many_class_name = pluralized_symbolized_class_name_from_table_name(fk.referenced_table_name)
80
+ self.send(:has_many, has_many_class_name, :through=>association[0])
81
+ logger.info("GENERATED ASSOCIATION: #{self.name} has_many #{has_many_class_name}, :through=>#{association[0]}")
82
+ end
83
+ end
84
+ end
85
+
86
+ end # end of class methods
87
+
88
+ end # end of class Base
89
+
90
+ end # end of module ActiveRecord
data/lib/base.rb ADDED
@@ -0,0 +1,136 @@
1
+ module ActiveRecord
2
+
3
+ class Base
4
+
5
+ # Stores a dictionary of table_name => class_name mappings to facilitate reverse lookup
6
+ # with acceptable performance
7
+ @@class_name_hash = {}
8
+
9
+ @@logger = Logger.new(STDERR)
10
+ @@logger.level = Logger::INFO
11
+
12
+
13
+ # ------------------------------------------------------------------------------------------------------------------------
14
+ # Class Methods
15
+ #
16
+ # This extension to ActiveRecord::Base creates associations and validations for data objects
17
+ # based on the actual constraints defined on the DB that are retrieved through extension APIs on the
18
+ # database adapters.
19
+ #
20
+ # To accomplish this, it subverts the 2 methods of creating ActiveRecord objects:
21
+ #
22
+ # *1 - ActiveRecord::Base.initialize - this is called when an ActiveRecord object is created with ActiveRecord::Base.new
23
+ #
24
+ # *2 - ActiveRecord::Base.instantiate - this is called when an ActiveRecord object is created through one of the find methods
25
+ #
26
+ # When an ActiveRecord object is created through these methods the class-level associations and validations
27
+ # are generated unless they already exist
28
+ class << self
29
+
30
+ def logger
31
+ @@logger
32
+ end
33
+
34
+ # Save table name keys as all UPPERCASE to simplify lookup
35
+ alias :base_set_table_name :set_table_name
36
+ def set_table_name(value = nil, &block)
37
+ base_set_table_name(value)
38
+ @@class_name_hash["#{table_name.upcase}"] = self.name
39
+ end
40
+
41
+
42
+ # Overrides primary_key in ActiveRecord::Base
43
+ #
44
+ # This implementation will need to be adjusted should composite primary keys be supported in the future
45
+ #
46
+ # set_primary_key generates an accessor method on the caller that returns the string value
47
+ # --> this generated accessor will be invoked on future calls to primary_key
48
+ # ---------------------------------------------------------------------------------------------------
49
+ def primary_key
50
+ primary = table_constraints.detect {|constraint| constraint.primary_key?}
51
+ primary_name = primary.column_name
52
+ set_primary_key(primary_name)
53
+ primary_name
54
+ end
55
+
56
+
57
+ alias :base_class_name :class_name
58
+ def class_name(table_name = table_name) # :nodoc:
59
+ #Attempt to retrieve class name from cache before attempting to generate it
60
+ name = @@class_name_hash[table_name.upcase]
61
+ if !name
62
+ return base_class_name(table_name)
63
+ end
64
+ name
65
+ end
66
+
67
+
68
+ alias :base_instantiate :instantiate
69
+ def instantiate(record)
70
+ generate_associations
71
+ generate_validations
72
+ base_instantiate(record)
73
+ end
74
+
75
+ # table_constraints are those constraints defined on this table
76
+ def table_constraints
77
+ constraints.select {|constraint| constraint.referenced_table_name.nil? || constraint.table_name.upcase == table_name.upcase}
78
+ end
79
+
80
+ # foreign_constraints are those constraints that are defined on other tables, but
81
+ # reference this table (i.e. FKs into this table)
82
+ def foreign_constraints
83
+ constraints.select {|constraint|
84
+ !constraint.referenced_table_name.nil? &&
85
+ constraint.referenced_table_name.upcase == table_name.upcase &&
86
+ constraint.table_name.upcase != table_name.upcase}
87
+ end
88
+
89
+ def associations
90
+ @associations ? @associations : {}
91
+ end
92
+
93
+
94
+ private
95
+
96
+ def constraints
97
+ unless @constraints
98
+ @constraints = connection.constraints(table_name, "#{name} Constraints")
99
+ end
100
+ @constraints
101
+ end
102
+
103
+ # FIXME MacroReflection needs an instance method has_many?
104
+ # This logic does not belong in Base
105
+ def has_many_associations
106
+ reflections.select {|key, reflection| reflection.macro == :has_many}
107
+ end
108
+
109
+ def symbolized_class_name_from_table_name(table_name)
110
+ (class_name(table_name)).downcaseFirstLetter.to_sym
111
+ end
112
+
113
+ def pluralized_symbolized_class_name_from_table_name(table_name)
114
+ (class_name(table_name)).downcaseFirstLetter.pluralize.to_sym
115
+ end
116
+
117
+ end #end class << self (Class Methods)
118
+
119
+ # ---------------------------------------------------------------------------------------------------
120
+
121
+ public
122
+ alias :base_initialize :initialize
123
+ def initialize(attributes = nil)
124
+ base_initialize
125
+ self.class.generate_associations
126
+ self.class.generate_validations
127
+ end
128
+
129
+ def constraints
130
+ self.class.table_constraints
131
+ end
132
+
133
+
134
+ end # end class Base
135
+
136
+ end #end module ActiveRecord
@@ -0,0 +1,21 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters #:nodoc:
3
+
4
+ class Column
5
+
6
+ def generated?
7
+ raise NotImplementedError, "Column subclass did not implement this method"
8
+ end
9
+
10
+ # MySQL sinks the boat on this one.
11
+ # If no default is specified for a non-nullable column, then MySQL assigns an implicit default value.
12
+ # This makes it impossible to determine whether or not a default value has been specified for any
13
+ # non-nullable column without parsing the SHOW CREATE TABLE output, which is non-standard.
14
+ def default_specified?
15
+ raise NotImplementedError, "Column subclass did not implement this method"
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ module ActiveRecord
2
+
3
+ module ConnectionAdapters
4
+
5
+ # This is abstract class serves as the template for table constraints from all
6
+ # databases compatible with the ISO SQL:2003 information schema standard
7
+ class AbstractTableConstraint
8
+
9
+ def raise_subclass_responsibility_error
10
+ raise NotImplementedError, "AbstractTableConstraint subclass #{self.class} did not implement this method"
11
+ end
12
+
13
+ alias :primary_key? :raise_subclass_responsibility_error
14
+ alias :foreign_key? :raise_subclass_responsibility_error
15
+ alias :unique_key? :raise_subclass_responsibility_error
16
+ alias :component_of_unique_key? :raise_subclass_responsibility_error
17
+ alias :is_member_of_composite? :raise_subclass_responsibility_error
18
+ alias :constraint_schema :raise_subclass_responsibility_error
19
+ alias :constraint_name :raise_subclass_responsibility_error
20
+ alias :constraint_type :raise_subclass_responsibility_error
21
+ alias :table_schema :raise_subclass_responsibility_error
22
+ alias :table_name :raise_subclass_responsibility_error
23
+ alias :column_name :raise_subclass_responsibility_error
24
+ alias :referenced_table_schema :raise_subclass_responsibility_error
25
+ alias :referenced_table_name :raise_subclass_responsibility_error
26
+ alias :referenced_column_name :raise_subclass_responsibility_error
27
+
28
+ end
29
+
30
+
31
+ class AbstractAdapter
32
+
33
+ def constraints(table_name, name=nil)
34
+ raise NotImplementedError, "AbstractAdapter subclass #{self.class} did not implement this method"
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,139 @@
1
+ module ActiveRecord
2
+
3
+ module ConnectionAdapters
4
+
5
+ class MysqlColumn < Column #:nodoc:
6
+ attr_reader :extra
7
+
8
+ @@auto_increment = "auto_increment"
9
+
10
+ # Capture the value of the "EXTRA" column in the column metadata table.
11
+ # This can be used to generate validations
12
+ alias :base_initialize :initialize
13
+ def initialize(name, default, sql_type = nil, null = true, extra = nil)
14
+ base_initialize(name, default, sql_type, null)
15
+ @extra = extra
16
+ end
17
+
18
+ def generated?
19
+ @extra == @@auto_increment
20
+ end
21
+
22
+ # Until MySQL properly handles default values, I assume that a default value of NULL or empty
23
+ # string means that a default value has not been specified for the column.
24
+ #
25
+ # We really only care about this when the column is NOT NULL, in which case it should be safe
26
+ # to assume that no one would define the column as NOT NULL and then explicitly set the empty string
27
+ # as the default value. ...Right?
28
+ def default_specified?
29
+ !(default.nil? || default.blank?)
30
+ end
31
+
32
+ end
33
+
34
+
35
+ # A representation of a referential constraint or key in MySQL
36
+ # Specifically: Primary, Foreign, Unique Key definitions
37
+ class MysqlConstraint < AbstractTableConstraint
38
+ @@PRIMARY_KEY_TYPE='PRIMARY KEY'
39
+ @@FOREIGN_KEY_TYPE='FOREIGN KEY'
40
+ @@UNIQUE_KEY_TYPE='UNIQUE'
41
+
42
+ attr_reader :constraint_schema, :constraint_name, :constraint_type, :table_schema, :table_name, :column_name,
43
+ :referenced_table_schema, :referenced_table_name, :referenced_column_name
44
+
45
+ attr_writer :member_of_composite
46
+
47
+ def initialize(constraint_catalog, constraint_schema, constraint_name, table_schema, table_name, constraint_type,
48
+ column_name, referenced_table_schema, referenced_table_name, referenced_column_name)
49
+ @constraint_catalog = constraint_catalog
50
+ @constraint_schema = constraint_schema
51
+ @constraint_name = constraint_name
52
+ @table_schema = table_schema
53
+ @table_name = table_name
54
+ @constraint_type = constraint_type
55
+ @column_name = column_name
56
+ @referenced_table_schema = referenced_table_schema
57
+ @referenced_table_name = referenced_table_name
58
+ @referenced_column_name = referenced_column_name
59
+ end
60
+
61
+ def primary_key?
62
+ constraint_type == @@PRIMARY_KEY_TYPE
63
+ end
64
+
65
+ def foreign_key?
66
+ constraint_type == @@FOREIGN_KEY_TYPE
67
+ end
68
+
69
+ def component_of_unique_key?
70
+ constraint_type == @@UNIQUE_KEY_TYPE
71
+ end
72
+
73
+ def unique_key?
74
+ component_of_unique_key? and !is_member_of_composite?
75
+ end
76
+
77
+ def is_member_of_composite?
78
+ @member_of_composite ? @member_of_composite : false
79
+ end
80
+
81
+
82
+ end
83
+
84
+ # The purpose of this extension to MysqlAdapter is to exploit the referential constraints offered in MySQL >=5.0, and
85
+ # to make the table and column metadata stored in MySQL available to ActiveRecord objects
86
+ #
87
+ # Currently, CHECK constraints are not supported in MySQL 5.x. Although the syntax for creating CHECK constraints is
88
+ # supported, they are ignored by MySQL
89
+ class MysqlAdapter < AbstractAdapter
90
+
91
+ # FIXME filter SQL queries on information_schema by schema name in case a table
92
+ # with the same name exists in > 1 schema (i.e. test vs. dev schema)
93
+ def schema
94
+ @connection_options[3]
95
+ end
96
+
97
+ # The "EXTRA" column is discarded in the standard Rails implementation of MysqlAdapter.columns.
98
+ # ActiveRecord can use this column to generate validations
99
+ def columns(table_name, name = nil)#:nodoc:
100
+ sql = "SHOW FIELDS FROM #{table_name}"
101
+ columns = []
102
+ execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES", field[5])}
103
+ columns
104
+ end
105
+
106
+ # Retrieve each DB constraint from the information_schema database that is either a constraint on
107
+ # table_name or references table_name (eg. FK into table_name)
108
+ #
109
+ # Save a boolean value on each constraint indicating whether it is part of a composite constraint or not.
110
+ # This allows us to encapsulate is_composite? logic on the constraint object itself, rather than
111
+ # depending on access to the complete set of constraints for the table at a later time
112
+ def constraints(table_name, name = nil)#:nodoc:
113
+ constraints = []
114
+
115
+ sql = "select t2.*, t1.column_name, t1.referenced_table_schema, t1.referenced_table_name, t1.referenced_column_name \
116
+ from information_schema.key_column_usage as t1 inner join information_schema.table_constraints as t2 \
117
+ using (constraint_name, table_schema, table_name) where table_name='#{table_name}' or t1.referenced_table_name='#{table_name}'"
118
+ results = execute(sql, name)
119
+ constraint_name_hash = {}
120
+ results.each do |row|
121
+ constraints << MysqlConstraint.new(row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8], row[9])
122
+ comparable_constraint_name = row[2].upcase
123
+ constraint_name_count = constraint_name_hash[comparable_constraint_name]
124
+ constraint_name_count ?
125
+ constraint_name_hash[comparable_constraint_name] = constraint_name_count + 1 :
126
+ constraint_name_hash[comparable_constraint_name] = 1
127
+ end
128
+
129
+ constraints.each do | constraint|
130
+ constraint.member_of_composite=(constraint_name_hash[constraint.constraint_name.upcase] > 1)
131
+ end
132
+ constraints
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+
139
+ end
@@ -0,0 +1,116 @@
1
+ module ActiveRecord
2
+
3
+ module ConnectionAdapters
4
+
5
+ class PostgreSQLColumn < Column #:nodoc:
6
+ attr_accessor :generated
7
+
8
+ alias :generated? :generated
9
+
10
+ def default_specified?
11
+ !default.nil?
12
+ end
13
+
14
+ end
15
+
16
+
17
+ class PostgreSQLConstraint < AbstractTableConstraint
18
+ @@PRIMARY_KEY_TYPE = "PRIMARY KEY"
19
+ @@FOREIGN_KEY_TYPE = "FOREIGN KEY"
20
+ @@UNIQUE_KEY_TYPE = "UNIQUE"
21
+ @@CHECK_CONSTRAINT_TYPE = "CHECK"
22
+
23
+ attr_reader :constraint_catalog, :constraint_schema, :constraint_name, :constraint_type,
24
+ :table_catalog, :table_schema, :table_name, :column_name,
25
+ :referenced_table_catalog, :referenced_table_schema, :referenced_table_name, :referenced_column_name
26
+
27
+ attr_writer :member_of_composite
28
+
29
+ def initialize(table_catalog, table_schema, table_name, column_name, constraint_catalog, constraint_schema,
30
+ constraint_name, referenced_table_catalog, referenced_table_schema, referenced_table_name, constraint_type,
31
+ referenced_column_name)
32
+ @table_catalog = table_catalog
33
+ @table_schema = table_schema
34
+ @table_name = table_name
35
+ @column_name = column_name
36
+ @constraint_catalog = constraint_catalog
37
+ @constraint_schema = constraint_schema
38
+ @constraint_name = constraint_name
39
+ @constraint_type = constraint_type
40
+ @referenced_table_catalog = referenced_table_catalog
41
+ @referenced_table_schema = referenced_table_schema
42
+ @referenced_table_name = referenced_table_name
43
+ @referenced_column_name = referenced_column_name
44
+ end
45
+
46
+ def primary_key?
47
+ constraint_type == @@PRIMARY_KEY_TYPE
48
+ end
49
+
50
+ def foreign_key?
51
+ constraint_type == @@FOREIGN_KEY_TYPE
52
+ end
53
+
54
+ def check_constraint?
55
+ constraint_type == @@CHECK_CONSTRAINT_TYPE
56
+ end
57
+
58
+ def component_of_unique_key?
59
+ constraint_type == @@UNIQUE_KEY_TYPE
60
+ end
61
+
62
+ def unique_key?
63
+ component_of_unique_key? and !is_member_of_composite?
64
+ end
65
+
66
+ def is_member_of_composite?
67
+ @member_of_composite ? @member_of_composite : false
68
+ end
69
+
70
+ end
71
+
72
+
73
+ class PostgreSQLAdapter < AbstractAdapter
74
+
75
+ def columns(table_name, name = nil) #:nodoc:
76
+ columns = []
77
+ column_definitions(table_name).each do |name, type, default, notnull, typmod|
78
+ # typmod now unused as limit, precision, scale all handled by superclass
79
+ current = PostgreSQLColumn.new(name, default_value(default), translate_field_type(type), notnull == "f")
80
+ current.generated= (!default.nil? && default.index('nextval') == 0)
81
+ columns << current
82
+ end
83
+ columns
84
+ end
85
+
86
+
87
+ def constraints(table_name, name = nil)#:nodoc:
88
+ constraints = []
89
+ sql = "select t3.* , t4.* from information_schema.constraint_column_usage as t3 right join \
90
+ (select t2.*, t1.column_name from information_schema.key_column_usage as t1 inner join \
91
+ information_schema.table_constraints as t2 using (constraint_name, table_catalog, table_schema, table_name)) \
92
+ as t4 using (constraint_catalog, constraint_schema, constraint_name) \
93
+ where t4.table_name='#{table_name.downcase}' or t3.table_name='#{table_name.downcase}'"
94
+ results = query(sql, name)
95
+ constraint_name_hash = {}
96
+ results.each do |row|
97
+ constraints << PostgreSQLConstraint.new(row[10], row[11], row[12], row[16], row[4], row[5], row[6], row[0], row[1], row[2], row[13], row[3])
98
+ comparable_constraint_name = row[6].upcase
99
+ constraint_name_count = constraint_name_hash[comparable_constraint_name]
100
+ constraint_name_count ?
101
+ constraint_name_hash[comparable_constraint_name] = constraint_name_count + 1 :
102
+ constraint_name_hash[comparable_constraint_name] = 1
103
+ end
104
+
105
+ constraints.each do |constraint|
106
+ constraint.member_of_composite=(constraint_name_hash[constraint.constraint_name.upcase] > 1)
107
+ end
108
+ constraints
109
+ end
110
+
111
+ end
112
+
113
+
114
+ end
115
+
116
+ end
@@ -0,0 +1,24 @@
1
+ class Module #:nodoc:
2
+
3
+ alias :base_const_missing :const_missing
4
+
5
+ # If a non-existant class is specified, define it as a subclass of ActiveRecord::Base here
6
+ #
7
+ # This idea was inspired by the Magic Models project: http://magicmodels.rubyforge.org/
8
+ #
9
+ # I believe the on-demand generation and simple ActiveRecord::Base subclassing
10
+ # to be a better implementation for my needs
11
+ def const_missing(class_id)
12
+ begin
13
+ base_const_missing(class_id)
14
+ rescue NameError
15
+ class_def = <<-end_class_def
16
+ class #{class_id} < ActiveRecord::Base
17
+ end
18
+ end_class_def
19
+ eval(class_def, TOPLEVEL_BINDING)
20
+ end
21
+ const_get(class_id)
22
+ end
23
+
24
+ end
data/lib/drysql.rb ADDED
@@ -0,0 +1,14 @@
1
+ unless defined?(ActiveRecord)
2
+ require 'rubygems'
3
+ require_gem 'activerecord'
4
+ end
5
+
6
+ require 'connection_adapters/abstract_adapter'
7
+ require 'connection_adapters/mysql_adapter'
8
+ require 'connection_adapters/postgresql_adapter'
9
+ require 'connection_adapters/abstract/schema_definitions'
10
+ require 'associations'
11
+ require 'validations'
12
+ require 'helpers/string'
13
+ require 'base'
14
+ require 'dependencies'
@@ -0,0 +1,9 @@
1
+ class String
2
+
3
+ def downcaseFirstLetter
4
+ duplicate = self.dup
5
+ duplicate[0, 1] = duplicate[0, 1].downcase
6
+ duplicate
7
+ end
8
+
9
+ end
@@ -0,0 +1,107 @@
1
+ module ActiveRecord
2
+
3
+ class Errors
4
+
5
+ @@default_error_messages[:null] = "can't be null"
6
+
7
+ # Add an error message to each of the attributes in +attributes+ that is null
8
+ def add_on_null(attributes, msg = @@default_error_messages[:null])
9
+ for attr in [attributes].flatten
10
+ value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
11
+ add(attr, msg) if value.nil?
12
+ end
13
+ end
14
+
15
+ end
16
+
17
+
18
+ module Validations
19
+ module ClassMethods
20
+
21
+ # NOTE: template for this method borrowed from validates_presence_of method in Rails
22
+ def validates_nullability_of(*attr_names)
23
+ configuration = { :message => ActiveRecord::Errors.default_error_messages[:null], :on => :save }
24
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
25
+
26
+ # can't use validates_each here, because it cannot cope with nonexistent attributes,
27
+ # while errors.add_on_empty can
28
+ attr_names.each do |attr_name|
29
+ send(validation_method(configuration[:on])) do |record|
30
+ unless configuration[:if] and not evaluate_condition(configuration[:if], record)
31
+ record.errors.add_on_null(attr_name,configuration[:message])
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+
40
+
41
+ class Base
42
+
43
+ class << self
44
+
45
+ # The @validations instvar is used as an indicator as to whether the validations have already been generated or not.
46
+ # The first time an instance of a DrySQL-enabled subclass of ActiveRecord::Base is created, the validations are generated
47
+ # for the class.
48
+ #
49
+ # **NOTE: Some of this code and/or the ideas behind some of this code was borrowed from Simon Harris
50
+ # and Red Hill Consulting's Schema Validations plug-in. Thanks Simon!
51
+ # See http://www.redhillconsulting.com.au/rails_plugins.html#schema_validations
52
+ # for info about this plug-in
53
+ #
54
+ # Additional features added to Schema_Validations:
55
+ # 1) proper error message generated for boolean inclusion failure (:inclusion)
56
+ #
57
+ # 2) all validations are generated with allow_nil=>true because...
58
+ #
59
+ # 3) all validations are superceded with the _new_ validates_nullability_of,
60
+ # which generates an error for any field whose associated column has a NOT NULL constraint upon it,
61
+ # for whom no default value is specified, and for whom the value is not generated by the DB
62
+ #
63
+ # 4) do not generate validates_uniqueness_of by default for unique fields. This is a performance hit, and
64
+ # in my opinion completely defeats the purpose of an application-side data validation because it
65
+ # requires a query of the DB. Why not just let the DB do the work and handle the duplicate key exception?
66
+ #
67
+ # 5) Do not generate validates_presence_of for non-nullable fields. This will throw an exception for fields that
68
+ # contain an empty string, even though that may in fact be a valid value according to the table constraints.
69
+ # This approach also fails to consider that a default value might be specified for a non-nullable field in which case
70
+ # we do not need to block the null field from being saved to the DB
71
+ #
72
+ # 6) Perform validation auto-generation on all columns rather than just those returned by Base.content_columns
73
+ # I believe there is value in validating our PK, FK, and counter columns
74
+ def generate_validations
75
+ unless @validations
76
+ columns.each do |column|
77
+
78
+ if !(column.null || column.default_specified? || column.generated?)
79
+ self.validates_nullability_of column.name
80
+ logger.info("GENERATED VALIDATION: #{self.name} validates_nullability_of #{column.name}")
81
+ end
82
+
83
+ if column.type == :integer
84
+ self.validates_numericality_of column.name, :allow_nil => true, :only_integer => true
85
+ logger.info("GENERATED VALIDATION: #{self.name} validates_numericality_of #{column.name}, :allow_nil=>true, :only_integer=>true")
86
+ elsif column.number?
87
+ self.validates_numericality_of column.name, :allow_nil => true
88
+ logger.info("GENERATED VALIDATION: #{self.name} validates_numericality_of #{column.name}, :allow_nil=>true")
89
+ elsif column.text? && column.limit
90
+ self.validates_length_of column.name, :allow_nil => true, :maximum => column.limit
91
+ logger.info("GENERATED VALIDATION: #{self.name} validates_length_of #{column.name}, :allow_nil=>true, :maximum=>#{column.limit}")
92
+ elsif column.type == :boolean
93
+ self.validates_inclusion_of column.name, :in => [true, false], :allow_nil =>true, :message => ActiveRecord::Errors.default_error_messages[:inclusion]
94
+ logger.info("GENERATED VALIDATION: #{self.name} validates_inclusion_of #{column.name}, :in=>[true, false], \
95
+ :allow_nil=>true, :message=>ActiveRecord::Errors.default_error_messages[:inclusion]")
96
+ end
97
+ end
98
+ end
99
+ @validations=true
100
+ end
101
+
102
+
103
+ end # end of class methods
104
+
105
+ end # end of class Base
106
+
107
+ end # end of module ActiveRecord
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: drysql
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2006-10-30 00:00:00 -05:00
8
+ summary: Extends ActiveRecord to provide automatic identification of keys, and automatic generation of associations and validations
9
+ require_paths:
10
+ - lib
11
+ email: bsevans@rubyforge.org
12
+ homepage: http://drysql.rubyforge.org
13
+ rubyforge_project: drysql
14
+ description: DrySQL uses your DB's information schema to identify keys, and generate associations and validations for ActiveRecord objects. You defined the schema on your DB, don't re-define it in your application code. Legacy DB Tables/Column Names, Ruby Desktop apps, Rails apps...DrySQL's got all your (data)bases covered
15
+ autorequire: drysql
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: false
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Bryan Evans
30
+ files:
31
+ - lib/validations.rb
32
+ - lib/dependencies.rb
33
+ - lib/associations.rb
34
+ - lib/drysql.rb
35
+ - lib/base.rb
36
+ - lib/helpers/string.rb
37
+ - lib/connection_adapters/postgresql_adapter.rb
38
+ - lib/connection_adapters/mysql_adapter.rb
39
+ - lib/connection_adapters/abstract_adapter.rb
40
+ - lib/connection_adapters/abstract/schema_definitions.rb
41
+ - README
42
+ test_files: []
43
+
44
+ rdoc_options: []
45
+
46
+ extra_rdoc_files: []
47
+
48
+ executables: []
49
+
50
+ extensions: []
51
+
52
+ requirements: []
53
+
54
+ dependencies:
55
+ - !ruby/object:Gem::Dependency
56
+ name: activerecord
57
+ version_requirement:
58
+ version_requirements: !ruby/object:Gem::Version::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 1.14.4
63
+ version: