drysql 0.1.8 → 0.2.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/README +1 -1
- data/lib/associations.rb +31 -7
- data/lib/base.rb +35 -10
- data/lib/connection_adapters/abstract_adapter.rb +4 -4
- data/lib/connection_adapters/ibm_db2_adapter.rb +432 -0
- data/lib/connection_adapters/mysql_adapter.rb +15 -12
- data/lib/connection_adapters/oracle_adapter.rb +61 -27
- data/lib/connection_adapters/postgresql_adapter.rb +31 -18
- data/lib/connection_adapters/sqlserver_adapter.rb +79 -33
- data/lib/drysql.rb +3 -0
- metadata +3 -2
data/README
CHANGED
@@ -113,7 +113,7 @@ by the DB.
|
|
113
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
114
|
I will publish tests with the next release
|
115
115
|
|
116
|
-
Enjoy!
|
116
|
+
Enjoy, and please give me your feedback!
|
117
117
|
|
118
118
|
|
119
119
|
|
data/lib/associations.rb
CHANGED
@@ -32,8 +32,8 @@ module ActiveRecord
|
|
32
32
|
foreign_keys = table_constraints.select {|constraint| constraint.foreign_key?}
|
33
33
|
foreign_keys.each do |foreign_key|
|
34
34
|
belongs_to_class_name = (class_name(foreign_key.referenced_table_name)).downcaseFirstLetter
|
35
|
-
self.send(:belongs_to, :"#{belongs_to_class_name}", :foreign_key=>foreign_key.column_name)
|
36
|
-
logger.info("DRYSQL >> GENERATED ASSOCIATION: #{self.name} belongs_to :#{belongs_to_class_name}, :foreign_key=>#{foreign_key.column_name}")
|
35
|
+
self.send(:belongs_to, :"#{belongs_to_class_name}", :foreign_key=>foreign_key.column_name.to_a[0])
|
36
|
+
logger.info("DRYSQL >> GENERATED ASSOCIATION: #{self.name} belongs_to :#{belongs_to_class_name}, :foreign_key=>#{foreign_key.column_name.to_a[0]}")
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
@@ -43,12 +43,12 @@ module ActiveRecord
|
|
43
43
|
foreign_constraints.each do |foreign_constraint|
|
44
44
|
if foreign_constraint_column_is_unique?(foreign_constraint)
|
45
45
|
has_one_class_name = (class_name(foreign_constraint.table_name)).downcaseFirstLetter
|
46
|
-
self.send(:has_one, :"#{has_one_class_name}", :foreign_key=>foreign_constraint.
|
47
|
-
logger.info("DRYSQL >> GENERATED ASSOCIATION: #{self.name} has_one :#{has_one_class_name}, :foreign_key=>#{foreign_constraint.
|
46
|
+
self.send(:has_one, :"#{has_one_class_name}", :foreign_key=>foreign_constraint.column_name.to_a[0])
|
47
|
+
logger.info("DRYSQL >> GENERATED ASSOCIATION: #{self.name} has_one :#{has_one_class_name}, :foreign_key=>#{foreign_constraint.column_name.to_a[0]}")
|
48
48
|
else
|
49
49
|
has_many_class_name = (class_name(foreign_constraint.table_name)).downcaseFirstLetter.pluralize
|
50
|
-
self.send(:has_many, :"#{has_many_class_name}", :foreign_key=>foreign_constraint.
|
51
|
-
logger.info("DRYSQL >> GENERATED ASSOCIATION: #{self.name} has_many :#{has_many_class_name}, :foreign_key=>#{foreign_constraint.
|
50
|
+
self.send(:has_many, :"#{has_many_class_name}", :foreign_key=>foreign_constraint.column_name.to_a[0])
|
51
|
+
logger.info("DRYSQL >> GENERATED ASSOCIATION: #{self.name} has_many :#{has_many_class_name}, :foreign_key=>#{foreign_constraint.column_name.to_a[0]}")
|
52
52
|
end
|
53
53
|
end
|
54
54
|
end
|
@@ -56,7 +56,9 @@ module ActiveRecord
|
|
56
56
|
def foreign_constraint_column_is_unique?(constraint)
|
57
57
|
class_name = class_name(constraint.table_name)
|
58
58
|
klass = instance_eval(class_name)
|
59
|
-
|
59
|
+
# FIXME Is there a more efficient way of doing this?
|
60
|
+
# The uniqueness check requires that all constraints for each table that references the current table must be retrieved
|
61
|
+
constraints_on_given_column = klass.table_constraints.select {|current| current.column_name.include?(constraint.column_name.to_a[0])}
|
60
62
|
constraints_on_given_column.any? {|current| current.unique_key?}
|
61
63
|
end
|
62
64
|
|
@@ -92,4 +94,26 @@ module ActiveRecord
|
|
92
94
|
|
93
95
|
end # end of class Base
|
94
96
|
|
97
|
+
|
98
|
+
module Associations
|
99
|
+
|
100
|
+
module ClassMethods
|
101
|
+
|
102
|
+
class JoinDependency # :nodoc:
|
103
|
+
|
104
|
+
# Intercept initialization of JoinDependency objects
|
105
|
+
# in order to ensure that associations have been generated
|
106
|
+
# before we attempt to create joins for eager loading
|
107
|
+
alias :base_initialize :initialize
|
108
|
+
def initialize(base, associations, joins)
|
109
|
+
base.generate_associations
|
110
|
+
base_initialize(base, associations, joins)
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end # end of class JoinDependency
|
116
|
+
|
117
|
+
end # end of module Associations
|
118
|
+
|
95
119
|
end # end of module ActiveRecord
|
data/lib/base.rb
CHANGED
@@ -48,10 +48,13 @@ module ActiveRecord
|
|
48
48
|
# ---------------------------------------------------------------------------------------------------
|
49
49
|
def primary_key
|
50
50
|
primary = table_constraints.detect {|constraint| constraint.primary_key?}
|
51
|
-
if primary.nil?
|
52
|
-
|
53
|
-
|
54
|
-
|
51
|
+
if primary.nil?
|
52
|
+
logger.error("DRYSQL >> No primary key defined for table #{table_name}")
|
53
|
+
else
|
54
|
+
primary_name = primary.column_name.to_a[0]
|
55
|
+
set_primary_key(primary_name)
|
56
|
+
logger.info("DRYSQL >> Identified PRIMARY KEY for #{self}: #{primary_name}")
|
57
|
+
end
|
55
58
|
primary_name
|
56
59
|
end
|
57
60
|
|
@@ -76,16 +79,13 @@ module ActiveRecord
|
|
76
79
|
|
77
80
|
# table_constraints are those constraints defined on this table
|
78
81
|
def table_constraints
|
79
|
-
constraints.
|
82
|
+
constraints.reject {|constraint| constraint.is_foreign_constraint?(table_name)}
|
80
83
|
end
|
81
84
|
|
82
85
|
# foreign_constraints are those constraints that are defined on other tables, but
|
83
86
|
# reference this table (i.e. FKs into this table)
|
84
|
-
def foreign_constraints
|
85
|
-
constraints.select {|constraint|
|
86
|
-
!constraint.referenced_table_name.nil? &&
|
87
|
-
constraint.referenced_table_name.upcase == table_name.upcase &&
|
88
|
-
constraint.table_name.upcase != table_name.upcase}
|
87
|
+
def foreign_constraints
|
88
|
+
constraints.select {|constraint| constraint.is_foreign_constraint?(table_name)}
|
89
89
|
end
|
90
90
|
|
91
91
|
def associations
|
@@ -114,6 +114,31 @@ module ActiveRecord
|
|
114
114
|
set_primary_key(primary_name)
|
115
115
|
logger.info("DRYSQL >> Reset PRIMARY KEY for #{self}: #{primary_name}")
|
116
116
|
end
|
117
|
+
|
118
|
+
|
119
|
+
# This method will generate associations and validations for all defined
|
120
|
+
# subclasses of ActiveRecord::Base.
|
121
|
+
#
|
122
|
+
# If any of the constraints processed refer to undefined model classes,
|
123
|
+
# these classes will be defined as long as they conform to the ActiveRecord
|
124
|
+
# naming conventions.
|
125
|
+
#
|
126
|
+
# This method swallows exceptions that occur during the generation of associations
|
127
|
+
# and validations, but logs these exceptions in detail to STDERR.
|
128
|
+
def generate_orm
|
129
|
+
models = @@subclasses.to_a.flatten.uniq
|
130
|
+
models.each do |model|
|
131
|
+
begin
|
132
|
+
if model != Base
|
133
|
+
model.generate_associations
|
134
|
+
model.generate_validations
|
135
|
+
end
|
136
|
+
rescue Exception
|
137
|
+
logger.error("DRYSQL ERROR >> Could not generate ORM for #{model}")
|
138
|
+
logger.error($!.backtrace.join("\n"))
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
117
142
|
|
118
143
|
|
119
144
|
private
|
@@ -11,9 +11,8 @@ module ActiveRecord
|
|
11
11
|
UNIQUE_KEY_TYPE = "UNIQUE"
|
12
12
|
CHECK_CONSTRAINT_TYPE = "CHECK"
|
13
13
|
|
14
|
-
attr_reader :
|
15
|
-
:
|
16
|
-
:referenced_table_catalog, :referenced_table_schema, :referenced_table_name, :referenced_column_name
|
14
|
+
attr_reader :constraint_name, :constraint_type, :table_schema, :table_name, :column_name,
|
15
|
+
:referenced_table_name, :referenced_column_name
|
17
16
|
|
18
17
|
attr_writer :member_of_composite
|
19
18
|
|
@@ -22,6 +21,7 @@ module ActiveRecord
|
|
22
21
|
end
|
23
22
|
|
24
23
|
alias :initialize :raise_subclass_responsibility_error
|
24
|
+
alias :is_foreign_constraint? :raise_subclass_responsibility_error
|
25
25
|
|
26
26
|
def primary_key?
|
27
27
|
constraint_type == PRIMARY_KEY_TYPE
|
@@ -51,7 +51,7 @@ module ActiveRecord
|
|
51
51
|
def constraints(table_name, name=nil)
|
52
52
|
raise NotImplementedError, "AbstractAdapter subclass #{self.class} did not implement this method"
|
53
53
|
end
|
54
|
-
|
54
|
+
|
55
55
|
end
|
56
56
|
end
|
57
57
|
end
|
@@ -0,0 +1,432 @@
|
|
1
|
+
# Most of this code is borrowed from IBM's rails adapter,
|
2
|
+
# which is authored by Antonio Cangiano
|
3
|
+
#
|
4
|
+
# I have made modifications to the IBM adapter code to support DB2 iSeries
|
5
|
+
# as well as to add features for DrySQL on DB2 LUW
|
6
|
+
|
7
|
+
module ActiveRecord
|
8
|
+
|
9
|
+
# These extensions in ActiveRecord::Base are for iSeries support
|
10
|
+
class Base
|
11
|
+
|
12
|
+
class << self
|
13
|
+
|
14
|
+
alias_method :ibm_find_one, :find_one
|
15
|
+
# Finds a record based on the +id+ value. The column associated with the primary key
|
16
|
+
# can be of a numeric type however DB2 doesn't allow quoted numeric values (e.g WHERE id = '10').
|
17
|
+
# Therefore this overrides the default method to check for the column type,
|
18
|
+
# and leaves the value in the query unquoted in cases of numeric columns
|
19
|
+
def find_one(id, options)
|
20
|
+
# If the adapter in use is the IBM DB2 Adapter
|
21
|
+
if connection.kind_of?(ConnectionAdapters::IBM_DB2Adapter)
|
22
|
+
if !connection.iseries then return ibm_find_one(id, options) end
|
23
|
+
|
24
|
+
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
|
25
|
+
# Retrieves the sql type of the column associated to the primary key
|
26
|
+
cstmt = connection.execute("SELECT coltype FROM SYSCOLUMNS WHERE \
|
27
|
+
table_name ='#{table_name.upcase}' AND column_name ='#{primary_key.upcase}'")
|
28
|
+
DB2::fetch_row(cstmt)
|
29
|
+
primary_key_type = DB2::result(cstmt, 0)
|
30
|
+
|
31
|
+
# Frees the results set associated with the statement
|
32
|
+
DB2::free_result(cstmt)
|
33
|
+
|
34
|
+
# If the column is a numeric type
|
35
|
+
if primary_key_type =~ /int|double|real|decimal|numeric/i
|
36
|
+
# Assign the unquoted id value to san_id
|
37
|
+
san_id = sanitize_numeric(id)
|
38
|
+
else
|
39
|
+
# The column is not numeric, so +sanitize+ it
|
40
|
+
san_id = sanitize(id)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Adds the options based on the san_id value
|
44
|
+
options.update :conditions => "#{table_name}.#{primary_key} = #{san_id}#{conditions}"
|
45
|
+
|
46
|
+
if result = find_initial(options)
|
47
|
+
result
|
48
|
+
else
|
49
|
+
raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
|
50
|
+
end
|
51
|
+
else
|
52
|
+
# You're not using the IBM DB2 Adapter therefore the default method is recalled
|
53
|
+
default_find_one(id, options)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
alias_method :ibm_find_some, :find_some
|
59
|
+
# Finds a record based on a list of +ids+. The primary key column type could be numeric,
|
60
|
+
# and DB2 doesn't allow quoted numeric values. Therefore this version of the method
|
61
|
+
# checks for the data type and if it's a numeric type the value in the query is unquoted.
|
62
|
+
# If the column datatype is not numeric, the value is properly quoted/sanitized
|
63
|
+
def find_some(ids, options)
|
64
|
+
# If the adapter in use is the IBM DB2 Adapter
|
65
|
+
if connection.kind_of?(ConnectionAdapters::IBM_DB2Adapter)
|
66
|
+
if !connection.iseries then return ibm_find_some(ids, options) end
|
67
|
+
|
68
|
+
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
|
69
|
+
# Retrieves the sql type of the column associated to the primary key
|
70
|
+
cstmt = connection.execute("SELECT typename FROM SYSCOLUMNS WHERE \
|
71
|
+
table_name ='#{table_name.upcase}' AND column_name ='#{primary_key.upcase}'")
|
72
|
+
DB2::fetch_row(cstmt)
|
73
|
+
primary_key_type = DB2::result(cstmt, 0)
|
74
|
+
|
75
|
+
# Frees the results set associated with the statement
|
76
|
+
DB2::free_result(cstmt)
|
77
|
+
|
78
|
+
# If the column is a numeric type
|
79
|
+
if primary_key_type =~ /int|double|real|decimal|numeric/i
|
80
|
+
# Generates a comma separated list of ids
|
81
|
+
ids_list = ids.map {|id| sanitize_numeric(id) }.join(',')
|
82
|
+
else
|
83
|
+
# Generates a comma separated list of quoted/sanitized ids
|
84
|
+
ids_list = ids.map { |id| sanitize(id) }.join(',')
|
85
|
+
end
|
86
|
+
|
87
|
+
# Adds the options to the query, based on the generated +ids_list+
|
88
|
+
options.update :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}"
|
89
|
+
|
90
|
+
result = find_every(options)
|
91
|
+
|
92
|
+
if result.size == ids.size
|
93
|
+
result
|
94
|
+
else
|
95
|
+
raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
|
96
|
+
end
|
97
|
+
else
|
98
|
+
# You're not using the IBM DB2 Adapter therefore the default method is recalled
|
99
|
+
default_find_some(ids, options)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end # class << self
|
103
|
+
end # class Base
|
104
|
+
|
105
|
+
|
106
|
+
module ConnectionAdapters
|
107
|
+
|
108
|
+
class IBM_DB2Column < Column
|
109
|
+
attr_accessor :generated, :default_specified
|
110
|
+
|
111
|
+
def initialize(name, default, generated, sql_type = nil, null = true)
|
112
|
+
@name, @type, @null = name, simplified_type(sql_type), null
|
113
|
+
@text = [:string, :text, :binary].include? @type
|
114
|
+
@sql_type = sql_type
|
115
|
+
# have to do this one separately because type_cast depends on #type
|
116
|
+
if !(default.nil? || default.blank?) then @default_specified = true end
|
117
|
+
@default = type_cast(default)
|
118
|
+
# Extracts the limit if it's a :string or :text type
|
119
|
+
@limit = extract_limit(sql_type) if !sql_type.nil? && @text
|
120
|
+
@primary = nil
|
121
|
+
@number = [:float, :integer].include? @type
|
122
|
+
@generated = generated
|
123
|
+
end
|
124
|
+
|
125
|
+
def default_specified?
|
126
|
+
@default_specified
|
127
|
+
end
|
128
|
+
|
129
|
+
def generated?
|
130
|
+
!(generated.nil? || generated.blank?)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
class IBM_DB2Constraint < AbstractTableConstraint
|
136
|
+
|
137
|
+
attr_reader :update_rule, :delete_rule, :referenced_constraint_name
|
138
|
+
|
139
|
+
ISERIES_PRIMARY_KEY_TYPE = "P"
|
140
|
+
ISERIES_FOREIGN_KEY_TYPE = "F"
|
141
|
+
ISERIES_UNIQUE_KEY_TYPE = "U"
|
142
|
+
ISERIES_CHECK_CONSTRAINT_TYPE = "C"
|
143
|
+
|
144
|
+
def initialize(constraint_schema, constraint_name, constraint_type, table_name, column_name,
|
145
|
+
referenced_constraint_name, referenced_table_name, referenced_column_name, delete_rule)
|
146
|
+
@constraint_schema = constraint_schema
|
147
|
+
@constraint_name = constraint_name
|
148
|
+
@table_name = table_name
|
149
|
+
@constraint_type = constraint_type
|
150
|
+
if !column_name.nil? then @column_name = Set.new [column_name.downcase] end
|
151
|
+
@referenced_table_name = referenced_table_name
|
152
|
+
@referenced_constraint_name = referenced_constraint_name
|
153
|
+
@delete_rule = delete_rule
|
154
|
+
if !referenced_column_name.nil? then @referenced_column_name = referenced_column_name.downcase end
|
155
|
+
end
|
156
|
+
|
157
|
+
def primary_key?
|
158
|
+
constraint_type == PRIMARY_KEY_TYPE || constraint_type == ISERIES_PRIMARY_KEY_TYPE
|
159
|
+
end
|
160
|
+
|
161
|
+
def foreign_key?
|
162
|
+
constraint_type == FOREIGN_KEY_TYPE || constraint_type == ISERIES_FOREIGN_KEY_TYPE
|
163
|
+
end
|
164
|
+
|
165
|
+
def component_of_unique_key?
|
166
|
+
constraint_type == UNIQUE_KEY_TYPE || constraint_type == ISERIES_UNIQUE_KEY_TYPE
|
167
|
+
end
|
168
|
+
|
169
|
+
def is_member_of_composite?
|
170
|
+
@column_name.size > 1
|
171
|
+
end
|
172
|
+
|
173
|
+
def is_foreign_constraint?(table_name)
|
174
|
+
@table_name.upcase != table_name.upcase
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
class IBM_DB2Adapter < AbstractAdapter
|
181
|
+
attr_reader :iseries
|
182
|
+
|
183
|
+
alias :base_initialize :initialize
|
184
|
+
def initialize(connection, logger, config)
|
185
|
+
if (config[:platform] == "iseries")
|
186
|
+
@iseries = true
|
187
|
+
logger.info("DRYSQL >> iSeries platform detected")
|
188
|
+
end
|
189
|
+
base_initialize(connection, logger, config)
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
alias :base_columns :columns
|
194
|
+
# Returns an array of Column objects for the table specified by +table_name+
|
195
|
+
# This method re-definition is valid only for iSeries
|
196
|
+
def columns(table_name, name = nil)
|
197
|
+
# to_s required because it may be a symbol.
|
198
|
+
table_name = table_name.to_s.upcase
|
199
|
+
# Checks if a blank table name has been given.
|
200
|
+
# If so it returns an empty array
|
201
|
+
return [] if table_name.strip.empty?
|
202
|
+
# +columns+ will contain the resulting array
|
203
|
+
columns = []
|
204
|
+
if @iseries
|
205
|
+
sql = "SELECT column_name, dftvalue as column_default, coltype as type, length, nulls, generated FROM SYSCOLUMNS WHERE TBNAME = '#{table_name}' AND DBNAME = '#{@schema.upcase}' ORDER BY colno"
|
206
|
+
else
|
207
|
+
sql = "SELECT colname as column_name, default as column_default, typename as type, length, nulls, generated FROM SYSCAT.COLUMNS WHERE TABNAME = '#{table_name}' AND TABSCHEMA = '#{@schema.upcase}' ORDER BY colno"
|
208
|
+
end
|
209
|
+
# Statement required to access all the columns information
|
210
|
+
stmt = execute(sql, name)
|
211
|
+
# Fetches all the columns and assigns them to col.
|
212
|
+
# +col+ is an hash with keys/value pairs for a column
|
213
|
+
while col = DB2::fetch_assoc(stmt)
|
214
|
+
column_name = col["column_name"].downcase
|
215
|
+
# Assigns the column default value.
|
216
|
+
column_default_value = col["column_default"]
|
217
|
+
# If there is no default value, it assigns NIL
|
218
|
+
column_default_value = nil if (column_default_value && column_default_value.upcase == 'NULL')
|
219
|
+
# Removes single quotes from the default value
|
220
|
+
column_default_value.gsub!(/^'(.*)'$/, '\1') unless column_default_value.nil?
|
221
|
+
# Assigns the column type
|
222
|
+
column_type = col["type"].downcase
|
223
|
+
# Assigns the field length (size) for the column
|
224
|
+
column_length = col["length"]
|
225
|
+
# The initializer of the class Column, requires the +column_length+ to be declared between brackets after
|
226
|
+
# the datatype(e.g VARCHAR(50)) for :string and :text types. If it's a "for bit data" field it does a subsitution in place, if not
|
227
|
+
# it appends the (column_length) string on the supported data types
|
228
|
+
unless column_length.nil? || column_length == '' || column_type.sub!(/ \(\) for bit data/i,"(#{column_length}) FOR BIT DATA") || !column_type =~ /char|lob|graphic/i
|
229
|
+
column_type << "(#{column_length})"
|
230
|
+
end
|
231
|
+
# col["NULLABLE"] is 1 if the field is nullable, 0 if not.
|
232
|
+
column_nullable = col["nulls"] == 'Y' ? true : false
|
233
|
+
generated = col["generated"]
|
234
|
+
# Pushes into the array the *IBM_DB2Column* object, created by passing to the initializer
|
235
|
+
# +column_name+, +default_value+, +column_type+ and +column_nullable+.
|
236
|
+
columns << IBM_DB2Column.new(column_name, column_default_value, generated, column_type, column_nullable)
|
237
|
+
end
|
238
|
+
# Returns the columns array
|
239
|
+
columns
|
240
|
+
end
|
241
|
+
|
242
|
+
alias :base_indexes :indexes
|
243
|
+
# Returns an array of indexes for the given table, skipping the primary key.
|
244
|
+
# The current implementation accesses SYSINDEXES and QSYS2.SYSKEYS.
|
245
|
+
# This method is valid only for iSeries
|
246
|
+
def indexes(table_name, name = nil)
|
247
|
+
if !@iseries then return base_indexes(table_name, name) end
|
248
|
+
|
249
|
+
# to_s required because +table_name+ may be a symbol.
|
250
|
+
table_name = table_name.to_s
|
251
|
+
# Checks if a blank table name has been given.
|
252
|
+
# If so it returns an empty array of columns.
|
253
|
+
return [] if table_name.strip.empty?
|
254
|
+
# +indexes+ will contain the resulting array
|
255
|
+
indexes = []
|
256
|
+
# Query used to retrieve all the indexes from the given table
|
257
|
+
sql = "select * from ((SELECT index_name, is_unique FROM SYSINDEXES WHERE table_name = '#{table_name.upcase}' AND table_schema = '#{@schema.upcase}') \
|
258
|
+
as IND inner join (select index_name, column_name from QSYS2.SYSKEYS where index_schema='#{@schema.upcase}') as KEYS on (IND.index_name=KEYS.index_name))"
|
259
|
+
stmt = execute(sql, name)
|
260
|
+
klass = instance_eval(ActiveRecord::Base.class_name(table_name))
|
261
|
+
primary_key = klass.primary_key
|
262
|
+
index_hash = {}
|
263
|
+
# Fetches all the records available
|
264
|
+
while table_index = DB2::fetch_assoc(stmt)
|
265
|
+
# Gets the lowercased index name
|
266
|
+
index_name = table_index["index_name"].downcase
|
267
|
+
# Is the index a primary key?
|
268
|
+
unless index_name.upcase == primary_key.upcase
|
269
|
+
# Is the index unique?
|
270
|
+
index_unique = table_index["is_unique"] == 'U'
|
271
|
+
if index_hash[index_name].nil?
|
272
|
+
index_hash[index_name] = [index_unique, [column_name]]
|
273
|
+
else
|
274
|
+
index_hash[index_name][1] << column_name
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
# Creates IndexDefinition objects and adds them to the indexes array
|
279
|
+
index_hash.keys.each do |key|
|
280
|
+
current_index = index_hash[key]
|
281
|
+
indexes << IndexDefinition.new(table_name, key, current_index[0], current_index[1])
|
282
|
+
end
|
283
|
+
|
284
|
+
# Returns the indexes array
|
285
|
+
return indexes
|
286
|
+
# Ensures to free the resources associated with the statement
|
287
|
+
ensure
|
288
|
+
DB2::free_result(stmt) if stmt
|
289
|
+
end
|
290
|
+
|
291
|
+
|
292
|
+
# DrySQL queries the current schema's System Catalog Views, which contain metadata
|
293
|
+
# only for those constraints that are defined in the current schema.
|
294
|
+
#
|
295
|
+
# If you have inter-schema referential constraints, and I'm not sure why you would or whether
|
296
|
+
# DB2 even supports this, DrySQL will not detect them and will raise an error.
|
297
|
+
def constraints(table_name, name = nil)#:nodoc:
|
298
|
+
constraints = []
|
299
|
+
if @iseries
|
300
|
+
return iseries_constraints(table_name, name)
|
301
|
+
else
|
302
|
+
sql = "select CST.tabschema, CST.constname, CST.tabname, CST.type, COL.colname, \
|
303
|
+
REF.constname as foreign_constraint_name, REF.tabname as foreign_table_name, \
|
304
|
+
REF.refkeyname, REF.reftabname, REF.deleterule, REF.fk_colnames as foreign_columns, \
|
305
|
+
REF.pk_colnames as referenced_columns from \
|
306
|
+
((select * from SYSCAT.TABCONST where tabname='#{table_name}') as CST \
|
307
|
+
inner join (select colname, constname from SYSCAT.KEYCOLUSE where tabname='#{table_name}') \
|
308
|
+
as COL on (CST.constname=COL.constname)) left outer join SYSCAT.REFERENCES REF on \
|
309
|
+
(CST.constname=REF.refkeyname or CST.constname=REF.constname)"
|
310
|
+
end
|
311
|
+
results = execute(sql, name)
|
312
|
+
constraint_name_hash = {}
|
313
|
+
|
314
|
+
# Note that column names in constraint objects are downcased in order to
|
315
|
+
# be comparable with the column names produced by IBM_DB2Adapter.columns
|
316
|
+
while row = DB2::fetch_assoc(results)
|
317
|
+
constraint_name = row['constname']
|
318
|
+
foreign_constraint_name = row['foreign_constraint_name']
|
319
|
+
|
320
|
+
# Process constraints local to this table
|
321
|
+
if !constraint_name_hash.has_key?(constraint_name)
|
322
|
+
current_constraint = IBM_DB2Constraint.new(row['tabschema'], constraint_name, row['type'],
|
323
|
+
row['tabname'], row['colname'], row['refkeyname'], row['reftabname'],
|
324
|
+
row['referenced_columns'], row['deleterule'])
|
325
|
+
constraints << current_constraint
|
326
|
+
constraint_name_hash[constraint_name] = current_constraint
|
327
|
+
# This key is a composite
|
328
|
+
else
|
329
|
+
current_constraint = constraint_name_hash[constraint_name]
|
330
|
+
# Unique Keys are currently the only type of composite keys supported
|
331
|
+
if current_constraint.component_of_unique_key?
|
332
|
+
current_constraint.column_name.add(row['colname'])
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
# Process constraints that reference this table's local constraints
|
337
|
+
if !foreign_constraint_name.nil? && !constraint_name_hash.has_key?(foreign_constraint_name)
|
338
|
+
current_foreign_constraint = IBM_DB2Constraint.new(row['tabschema'], foreign_constraint_name, IBM_DB2Constraint::FOREIGN_KEY_TYPE,
|
339
|
+
row['foreign_table_name'], row['foreign_columns'], constraint_name, row['tabname'], row['colname'],
|
340
|
+
row['deleterule'])
|
341
|
+
constraints << current_foreign_constraint
|
342
|
+
constraint_name_hash[foreign_constraint_name] = current_foreign_constraint
|
343
|
+
# Composite FKs are currently not supported
|
344
|
+
# else
|
345
|
+
# constraint_name_hash[foreign_constraint_name].column_name.add(row['foreign_column_name'])
|
346
|
+
end
|
347
|
+
end
|
348
|
+
constraints
|
349
|
+
end
|
350
|
+
|
351
|
+
# This method retrieves constraint information from the iSeries
|
352
|
+
# information schema views, which DB2 generates automatically
|
353
|
+
# for a schema when it (the schema) is created.
|
354
|
+
#
|
355
|
+
# Note that the old-style iSeries "libraries" that some people consider to
|
356
|
+
# by synonymous with "schemas" do not get the benefit of the information
|
357
|
+
# schema views (or journaling, for that matter). DrySQL will not
|
358
|
+
# play nicely with iSeries libraries unless you create the information
|
359
|
+
# schema views.
|
360
|
+
def iseries_constraints(table_name, name = nil)#:nodoc:
|
361
|
+
constraints = []
|
362
|
+
sql = "select CST.constraint_schema, CST.constraint_name, CST.table_name, CST.constraint_type, COL.column_name, \
|
363
|
+
REF.constraint_name as foreign_constraint_name, REF.unique_constraint_name as referenced_constraint_name, \
|
364
|
+
REF.delete_rule, COLREF.table_name as foreign_table_name, COLREF.column_name as foreign_column_name from \
|
365
|
+
((select * from SYSCST where table_name='#{table_name}') as CST \
|
366
|
+
inner join (select column_name, constraint_name from SYSCSTCOL where table_name='#{table_name}') \
|
367
|
+
as COL on (CST.constraint_name=COL.constraint_name) left outer join SYSREFCST REF on \
|
368
|
+
(CST.constraint_name=REF.unique_constraint_name or CST.constraint_name=REF.constraint_name) \
|
369
|
+
left join SYSCSTCOL AS COLREF on (NOT COLREF.table_name='#{table_name}' AND \
|
370
|
+
(REF.unique_constraint_name=COLREF.constraint_name or REF.constraint_name=COLREF.constraint_name)))"
|
371
|
+
results = execute(sql, name)
|
372
|
+
constraint_name_hash = {}
|
373
|
+
|
374
|
+
# Note that column names in constraint objects are downcased in order to
|
375
|
+
# be comparable with the column names produced by IBM_DB2Adapter.columns
|
376
|
+
while row = DB2::fetch_assoc(results)
|
377
|
+
constraint_name = row['constraint_name']
|
378
|
+
foreign_constraint_name = row['foreign_constraint_name']
|
379
|
+
|
380
|
+
# Process constraints local to this table
|
381
|
+
if !constraint_name_hash.has_key?(constraint_name)
|
382
|
+
current_constraint = IBM_DB2Constraint.new(row['constraint_schema'], constraint_name, row['constraint_type'],
|
383
|
+
row['table_name'], row['column_name'], row['referenced_constraint_name'], row['foreign_table_name'],
|
384
|
+
row['foreign_column_name'], row['delete_rule'])
|
385
|
+
constraints << current_constraint
|
386
|
+
constraint_name_hash[constraint_name] = current_constraint
|
387
|
+
# This key is a composite
|
388
|
+
else
|
389
|
+
current_constraint = constraint_name_hash[constraint_name]
|
390
|
+
# Unique Keys are currently the only type of composite keys supported
|
391
|
+
if current_constraint.component_of_unique_key?
|
392
|
+
current_constraint.column_name.add(row['column_name'])
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
# Process constraints that reference this table's local constraints
|
397
|
+
if !foreign_constraint_name.nil? && !constraint_name_hash.has_key?(foreign_constraint_name)
|
398
|
+
current_foreign_constraint = IBM_DB2Constraint.new(row['constraint_schema'], constraint_name, IBM_DB2Constraint::FOREIGN_KEY_TYPE,
|
399
|
+
row['foreign_table_name'], row['foreign_column_name'], foreign_constraint_name, row['table_name'], row['column_name'],
|
400
|
+
row['delete_rule'])
|
401
|
+
constraints << current_foreign_constraint
|
402
|
+
constraint_name_hash[foreign_constraint_name] = current_foreign_constraint
|
403
|
+
# Composite FKs are currently not supported
|
404
|
+
# else
|
405
|
+
# constraint_name_hash[foreign_constraint_name].column_name.add(row['foreign_column_name'])
|
406
|
+
end
|
407
|
+
end
|
408
|
+
constraints
|
409
|
+
end
|
410
|
+
|
411
|
+
|
412
|
+
alias :base_last_generated_id :last_generated_id
|
413
|
+
# Private methods that returns the last automatically generated ID
|
414
|
+
# on the given +@connection+. This method is required by the +insert+ method
|
415
|
+
# This method is valid only for iSeries
|
416
|
+
def last_generated_id
|
417
|
+
if !@iseries then return base_last_generated_id end
|
418
|
+
|
419
|
+
# Queries the db to obtain the last ID that was automatically generated
|
420
|
+
stmt = execute("SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1")
|
421
|
+
# Fetches the only record available (containing the last id)
|
422
|
+
DB2::fetch_row(stmt)
|
423
|
+
# Retrieves and returns the result of the query with the last id.
|
424
|
+
DB2::result(stmt,0)
|
425
|
+
end
|
426
|
+
private :last_generated_id
|
427
|
+
|
428
|
+
end
|
429
|
+
|
430
|
+
end
|
431
|
+
|
432
|
+
end
|
@@ -36,19 +36,22 @@ module ActiveRecord
|
|
36
36
|
# Specifically: Primary, Foreign, Unique Key definitions
|
37
37
|
class MysqlConstraint < AbstractTableConstraint
|
38
38
|
|
39
|
-
def initialize(
|
40
|
-
|
41
|
-
@constraint_catalog = constraint_catalog
|
42
|
-
@constraint_schema = constraint_schema
|
39
|
+
def initialize(constraint_name, table_schema, table_name, column_name, constraint_type,
|
40
|
+
referenced_table_name, referenced_column_name)
|
43
41
|
@constraint_name = constraint_name
|
44
42
|
@table_schema = table_schema
|
45
43
|
@table_name = table_name
|
46
44
|
@constraint_type = constraint_type
|
47
45
|
@column_name = column_name
|
48
|
-
@referenced_table_schema = referenced_table_schema
|
49
46
|
@referenced_table_name = referenced_table_name
|
50
47
|
@referenced_column_name = referenced_column_name
|
51
48
|
end
|
49
|
+
|
50
|
+
def is_foreign_constraint?(table_name)
|
51
|
+
!@referenced_table_name.nil? &&
|
52
|
+
@referenced_table_name.upcase == table_name.upcase &&
|
53
|
+
@table_name.upcase != table_name.upcase
|
54
|
+
end
|
52
55
|
|
53
56
|
end
|
54
57
|
|
@@ -82,15 +85,15 @@ module ActiveRecord
|
|
82
85
|
# depending on access to the complete set of constraints for the table at a later time
|
83
86
|
def constraints(table_name, name = nil)#:nodoc:
|
84
87
|
constraints = []
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
88
|
+
sql = "select KCU.constraint_name, KCU.table_schema, KCU.table_name, KCU.column_name, TC.constraint_type, KCU.referenced_table_name, KCU.referenced_column_name from \
|
89
|
+
((select constraint_name, constraint_type, table_name from information_schema.table_constraints where table_schema='#{schema}' and table_name='#{table_name}') as TC right join \
|
90
|
+
(select * from information_schema.key_column_usage where table_schema='#{schema}' and (table_name='#{table_name}' or referenced_table_name='#{table_name}')) as KCU on \
|
91
|
+
(TC.constraint_name=KCU.constraint_name AND TC.table_name=KCU.table_name))"
|
89
92
|
results = execute(sql, name)
|
90
93
|
constraint_name_hash = {}
|
91
94
|
results.each do |row|
|
92
|
-
constraints << MysqlConstraint.new(row[0], row[1], row[2], row[3], row[4], row[5], row[6]
|
93
|
-
comparable_constraint_name = row[2].upcase
|
95
|
+
constraints << MysqlConstraint.new(row[0], row[1], row[2], row[3], row[4], row[5], row[6])
|
96
|
+
comparable_constraint_name = row[0].upcase + row[2].upcase
|
94
97
|
constraint_name_count = constraint_name_hash[comparable_constraint_name]
|
95
98
|
constraint_name_count ?
|
96
99
|
constraint_name_hash[comparable_constraint_name] = constraint_name_count + 1 :
|
@@ -98,7 +101,7 @@ module ActiveRecord
|
|
98
101
|
end
|
99
102
|
|
100
103
|
constraints.each do | constraint|
|
101
|
-
constraint.member_of_composite=(constraint_name_hash[constraint.constraint_name.upcase] > 1)
|
104
|
+
constraint.member_of_composite=(constraint_name_hash[constraint.constraint_name.upcase + constraint.table_name.upcase] > 1)
|
102
105
|
end
|
103
106
|
constraints
|
104
107
|
end
|
@@ -4,10 +4,16 @@ module ActiveRecord
|
|
4
4
|
|
5
5
|
class OracleColumn < Column
|
6
6
|
|
7
|
-
attr_accessor :primary_key
|
7
|
+
attr_accessor :primary_key, :default_spceified
|
8
|
+
|
9
|
+
def initialize(name, default, sql_type = nil, null = true)
|
10
|
+
if !(default.nil? || default.blank?) then @default_specified = true end
|
11
|
+
super(name, default, sql_type, null)
|
12
|
+
end
|
13
|
+
|
8
14
|
|
9
15
|
def default_specified?
|
10
|
-
|
16
|
+
@default_specified
|
11
17
|
end
|
12
18
|
|
13
19
|
# The only means that Oracle currently provides for auto-generating a column
|
@@ -34,18 +40,24 @@ module ActiveRecord
|
|
34
40
|
UNIQUE_KEY_TYPE = "U"
|
35
41
|
CHECK_CONSTRAINT_TYPE = "C"
|
36
42
|
|
37
|
-
def initialize(
|
38
|
-
|
39
|
-
@constraint_catalog = constraint_catalog
|
40
|
-
@constraint_schema = constraint_schema
|
43
|
+
def initialize(constraint_name, constraint_type, table_name, column_name,
|
44
|
+
referenced_constraint_name, referenced_table_name, referenced_column_name, delete_rule)
|
41
45
|
@constraint_name = constraint_name
|
42
46
|
@table_schema = table_schema
|
43
47
|
@table_name = table_name
|
44
48
|
@constraint_type = constraint_type
|
45
|
-
@column_name = column_name
|
46
|
-
@referenced_table_schema = referenced_table_schema
|
49
|
+
@column_name = Set.new [column_name]
|
47
50
|
@referenced_table_name = referenced_table_name
|
48
51
|
@referenced_column_name = referenced_column_name
|
52
|
+
@delete_rule = delete_rule
|
53
|
+
end
|
54
|
+
|
55
|
+
def is_foreign_constraint?(table_name)
|
56
|
+
@table_name.upcase != table_name.upcase
|
57
|
+
end
|
58
|
+
|
59
|
+
def is_member_of_composite?
|
60
|
+
@column_name.size > 1
|
49
61
|
end
|
50
62
|
|
51
63
|
def primary_key?
|
@@ -82,36 +94,58 @@ module ActiveRecord
|
|
82
94
|
|
83
95
|
def constraints(table_name, name = nil)#:nodoc:
|
84
96
|
constraints = []
|
85
|
-
sql = "select
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
97
|
+
sql = "select UC.constraint_name, UC.constraint_type, UC.table_name, COL.column_name, \
|
98
|
+
REF.r_constraint_name as referenced_constraint_name, REF.constraint_name as foreign_constraint_name, \
|
99
|
+
REF.delete_rule as foreign_delete_rule, COLREF.table_name as foreign_table_name, COLREF.column_name as foreign_column_name from \
|
100
|
+
(select owner, constraint_name, constraint_type, table_name, r_owner, r_constraint_name \
|
101
|
+
from all_constraints where table_name='#{table_name}') UC inner join \
|
102
|
+
(select constraint_name, table_name, column_name from all_cons_columns where table_name='#{table_name}') COL on \
|
103
|
+
(COL.constraint_name = UC.constraint_name) left join all_constraints REF on \
|
104
|
+
(UC.constraint_name=REF.constraint_name OR UC.constraint_name=REF.r_constraint_name) left join all_cons_columns COLREF on \
|
105
|
+
(not COLREF.table_name='#{table_name}' AND (REF.constraint_name=COLREF.constraint_name OR REF.r_constraint_name=COLREF.constraint_name))"
|
90
106
|
|
91
107
|
results = select_all(sql, name)
|
92
108
|
constraint_name_hash = {}
|
93
109
|
results.each do |row|
|
94
|
-
|
95
110
|
# The author(s) of the Oracle adapter chose to selectively downcase column names for
|
96
111
|
# "cleanliness" within our Rails code. I had to follow suit with constraint column names
|
97
112
|
# so that model objects could look up their data values using the generated column accessor methods
|
98
|
-
|
99
|
-
unless row['
|
113
|
+
column_name = oracle_downcase(row['column_name'])
|
114
|
+
unless row['foreign_column_name'].nil? then row['foreign_column_name'] = oracle_downcase(row['foreign_column_name']) end
|
115
|
+
constraint_name = row['constraint_name']
|
116
|
+
foreign_constraint_name = row['foreign_constraint_name']
|
100
117
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
constraint_name_hash[
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
118
|
+
# Process constraints local to this table
|
119
|
+
if !constraint_name_hash.has_key?(constraint_name)
|
120
|
+
current_constraint = OracleConstraint.new(constraint_name, row['constraint_type'], row['table_name'],
|
121
|
+
column_name, row['referenced_constraint_name'], row['foreign_table_name'],
|
122
|
+
row['foreign_column_name'], row['foreign_delete_rule'])
|
123
|
+
constraints << current_constraint
|
124
|
+
constraint_name_hash[constraint_name] = current_constraint
|
125
|
+
# This key is a composite
|
126
|
+
else
|
127
|
+
current_constraint = constraint_name_hash[constraint_name]
|
128
|
+
# Unique Keys are currently the only type of composite keys supported
|
129
|
+
if current_constraint.component_of_unique_key?
|
130
|
+
current_constraint.column_name.add(column_name)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Process constraints that reference this table's local constraints
|
135
|
+
if !foreign_constraint_name.nil? && !constraint_name_hash.has_key?(foreign_constraint_name)
|
136
|
+
current_foreign_constraint = OracleConstraint.new(foreign_constraint_name, OracleConstraint::FOREIGN_KEY_TYPE,
|
137
|
+
row['foreign_table_name'], row['foreign_column_name'], constraint_name,
|
138
|
+
row['table_name'], row['column_name'], row['foreign_delete_rule'])
|
139
|
+
constraints << current_foreign_constraint
|
140
|
+
constraint_name_hash[foreign_constraint_name] = current_foreign_constraint
|
141
|
+
# Composite FKs are currently not supported
|
142
|
+
# else
|
143
|
+
# constraint_name_hash[foreign_constraint_name].column_name.add(row['foreign_column_name'])
|
144
|
+
end
|
112
145
|
end
|
113
146
|
constraints
|
114
147
|
end
|
148
|
+
|
115
149
|
|
116
150
|
end
|
117
151
|
|
@@ -3,12 +3,12 @@ module ActiveRecord
|
|
3
3
|
module ConnectionAdapters
|
4
4
|
|
5
5
|
class PostgreSQLColumn < Column #:nodoc:
|
6
|
-
attr_accessor :generated
|
6
|
+
attr_accessor :generated, :default_specified
|
7
7
|
|
8
8
|
alias :generated? :generated
|
9
9
|
|
10
10
|
def default_specified?
|
11
|
-
|
11
|
+
@default_specified
|
12
12
|
end
|
13
13
|
|
14
14
|
end
|
@@ -16,21 +16,26 @@ module ActiveRecord
|
|
16
16
|
|
17
17
|
class PostgreSQLConstraint < AbstractTableConstraint
|
18
18
|
|
19
|
-
def initialize(table_catalog, table_schema, table_name,
|
20
|
-
|
21
|
-
referenced_column_name)
|
19
|
+
def initialize(constraint_name, table_catalog, table_schema, table_name,
|
20
|
+
constraint_type, column_name, referenced_constraint_name, referenced_table_name,
|
21
|
+
referenced_column_name, delete_rule, update_rule)
|
22
22
|
@table_catalog = table_catalog
|
23
23
|
@table_schema = table_schema
|
24
24
|
@table_name = table_name
|
25
25
|
@column_name = column_name
|
26
|
-
@constraint_catalog = constraint_catalog
|
27
|
-
@constraint_schema = constraint_schema
|
28
26
|
@constraint_name = constraint_name
|
29
27
|
@constraint_type = constraint_type
|
30
|
-
@
|
31
|
-
@referenced_table_schema = referenced_table_schema
|
28
|
+
@referenced_constraint_name = referenced_constraint_name
|
32
29
|
@referenced_table_name = referenced_table_name
|
33
30
|
@referenced_column_name = referenced_column_name
|
31
|
+
@update_rule = update_rule
|
32
|
+
@delete_rule = delete_rule
|
33
|
+
end
|
34
|
+
|
35
|
+
def is_foreign_constraint?(table_name)
|
36
|
+
!@referenced_table_name.nil? &&
|
37
|
+
@referenced_table_name.upcase == table_name.upcase &&
|
38
|
+
@table_name.upcase != table_name.upcase
|
34
39
|
end
|
35
40
|
|
36
41
|
end
|
@@ -47,6 +52,7 @@ module ActiveRecord
|
|
47
52
|
# typmod now unused as limit, precision, scale all handled by superclass
|
48
53
|
current = PostgreSQLColumn.new(name, default_value(default), translate_field_type(type), notnull == "f")
|
49
54
|
current.generated= (!default.nil? && default.index('nextval') == 0)
|
55
|
+
current.default_specified = !(default.nil? || default.blank?)
|
50
56
|
columns << current
|
51
57
|
end
|
52
58
|
columns
|
@@ -54,23 +60,30 @@ module ActiveRecord
|
|
54
60
|
|
55
61
|
|
56
62
|
def constraints(table_name, name = nil)#:nodoc:
|
57
|
-
constraints = []
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
+
constraints = []
|
64
|
+
|
65
|
+
sql = "select t1.*, KCU.column_name, REF2.unique_constraint_name, KCU2.table_name as referenced_table_name, \
|
66
|
+
KCU2.column_name as referenced_column_name, REF2.delete_rule, REF2.update_rule from \
|
67
|
+
(select constraint_name, table_catalog, table_schema, table_name, constraint_type from \
|
68
|
+
information_schema.table_constraints where table_name='#{table_name.downcase}' or constraint_name in \
|
69
|
+
(select REF.constraint_name from information_schema.referential_constraints as REF
|
70
|
+
where unique_constraint_name in (select constraint_name from information_schema.table_constraints
|
71
|
+
where table_name='#{table_name.downcase}'))) as t1 inner join information_schema.key_column_usage as KCU on \
|
72
|
+
(t1.constraint_name=KCU.constraint_name) left join information_schema.referential_constraints as REF2 \
|
73
|
+
on (REF2.constraint_name=t1.constraint_name) left join information_schema.key_column_usage as KCU2 \
|
74
|
+
on (REF2.unique_constraint_name=KCU2.constraint_name)"
|
75
|
+
|
63
76
|
results = query(sql, name)
|
64
77
|
constraint_name_hash = {}
|
65
78
|
results.each do |row|
|
66
|
-
constraints << PostgreSQLConstraint.new(row[
|
67
|
-
comparable_constraint_name = row[
|
79
|
+
constraints << PostgreSQLConstraint.new(row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8], row[9], row[10])
|
80
|
+
comparable_constraint_name = row[0].upcase
|
68
81
|
constraint_name_count = constraint_name_hash[comparable_constraint_name]
|
69
82
|
constraint_name_count ?
|
70
83
|
constraint_name_hash[comparable_constraint_name] = constraint_name_count + 1 :
|
71
84
|
constraint_name_hash[comparable_constraint_name] = 1
|
72
85
|
end
|
73
|
-
|
86
|
+
|
74
87
|
constraints.each do |constraint|
|
75
88
|
constraint.member_of_composite=(constraint_name_hash[constraint.constraint_name.upcase] > 1)
|
76
89
|
end
|
@@ -4,19 +4,21 @@ module ActiveRecord
|
|
4
4
|
|
5
5
|
class SQLServerColumn < Column# :nodoc:
|
6
6
|
attr_reader :identity, :is_special
|
7
|
+
attr_accessor :default_specified
|
7
8
|
alias :generated? :identity
|
8
9
|
|
9
10
|
def default_specified?
|
10
|
-
|
11
|
+
@default_specified
|
11
12
|
end
|
12
13
|
|
13
14
|
# Borrowed verbatim from standard Rails sqlserver_adapter.
|
14
15
|
# Had to re-implement a bit of functionality here to avoid
|
15
16
|
# a dependency on SQLServerColumn.identity accessor
|
16
17
|
#
|
17
|
-
# Once the ActiveRecord gem
|
18
|
+
# Once the ActiveRecord gem includes the latest version of
|
18
19
|
# sqlserver_adapter, I'll be to remove this code
|
19
20
|
def initialize(name, default, sql_type = nil, identity = false, null = true)
|
21
|
+
if !(default.nil? || default.blank?) then @default_specified = true end
|
20
22
|
super(name, default, sql_type, null)
|
21
23
|
@identity = identity
|
22
24
|
@is_special = sql_type =~ /text|ntext|image/i
|
@@ -28,23 +30,28 @@ module ActiveRecord
|
|
28
30
|
|
29
31
|
class SQLServerConstraint < AbstractTableConstraint
|
30
32
|
|
31
|
-
|
32
|
-
|
33
|
-
constraint_catalog, constraint_schema, constraint_name, constraint_type)
|
34
|
-
@table_catalog = table_catalog
|
33
|
+
def initialize(constraint_schema, table_name, column_name, constraint_name, constraint_type,
|
34
|
+
referenced_constraint_name, referenced_table_name, referenced_column_name)
|
35
35
|
@table_schema = table_schema
|
36
36
|
@table_name = table_name
|
37
|
-
@column_name = column_name
|
38
|
-
@referenced_table_catalog = referenced_table_catalog
|
39
|
-
@referenced_table_schema = referenced_table_schema
|
37
|
+
@column_name = Set.new [column_name]
|
40
38
|
@referenced_table_name = referenced_table_name
|
41
39
|
@referenced_column_name = referenced_column_name
|
42
|
-
@constraint_catalog = constraint_catalog
|
43
40
|
@constraint_schema = constraint_schema
|
41
|
+
@referenced_constraint_name = referenced_constraint_name
|
44
42
|
@constraint_name = constraint_name
|
45
43
|
@constraint_type = constraint_type
|
46
44
|
end
|
47
45
|
|
46
|
+
|
47
|
+
def is_member_of_composite?
|
48
|
+
@column_name.size > 1
|
49
|
+
end
|
50
|
+
|
51
|
+
def is_foreign_constraint?(table_name)
|
52
|
+
@table_name.upcase != table_name.upcase
|
53
|
+
end
|
54
|
+
|
48
55
|
end
|
49
56
|
|
50
57
|
class SQLServerAdapter < AbstractAdapter
|
@@ -96,33 +103,72 @@ module ActiveRecord
|
|
96
103
|
end
|
97
104
|
|
98
105
|
|
106
|
+
# SQL Server allows you to duplicate table names & constraint names in a single database,
|
107
|
+
# provided that each constraint belongs to a different schema.
|
108
|
+
#
|
109
|
+
# The nice trickery in Rails and DrySQL that relies on retrieving info from the
|
110
|
+
# information_schema views will get confused if you create > 1 table with the same name in
|
111
|
+
# the same database or > 1 constraint with the same name.
|
112
|
+
#
|
113
|
+
# A future solution to this problem would be to have the ability to specify the desired schema
|
114
|
+
# in the database connection properties and have information_schema queries select only
|
115
|
+
# rows from the desired schema. Until this is implemented, do not duplicate table names
|
116
|
+
# or constraint names inside any DB.
|
117
|
+
#
|
118
|
+
# Constraints Query Optimization:
|
119
|
+
# -------------------------------
|
120
|
+
# - The current query seems to be optimal for the strategy of retrieving local table constraints
|
121
|
+
# as well as foreign keys into the current table in a single query
|
122
|
+
# - Since 4 tables need to be joined to return information about FKs into the current table
|
123
|
+
# the strategy is to join maximally reduced subsets of each table (i.e. rather than
|
124
|
+
# join the entire table, select minimum number of columns necessary and the minimum number of
|
125
|
+
# rows necessary from each table and then join the results)
|
126
|
+
# - Due to the layout of the information_schema views, the database will always need to scan every
|
127
|
+
# row in REFERENTIAL_CONSTRAINTS in order to stitch together table constraints and the FKs from
|
128
|
+
# other tables that reference these table constraints, which means that performance is inversely
|
129
|
+
# related to the number of FKs defined in the schema. Deleting unnecessary tables & FKs, and keeping
|
130
|
+
# logically separate groups of tables in separate schemas will improve performance
|
99
131
|
def constraints(table_name, name = nil)#:nodoc:
|
100
|
-
constraints = []
|
101
|
-
sql = "select
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
132
|
+
constraints = []
|
133
|
+
sql = "select TC.constraint_schema, TC.constraint_name, TC.table_name, TC.constraint_type, CCU.column_name, \
|
134
|
+
REF.constraint_name as foreign_constraint_name, REF.unique_constraint_name as referenced_constraint_name, REF.update_rule, \
|
135
|
+
REF.delete_rule, CCUREF.table_name as foreign_table_name, CCUREF.column_name as foreign_column_name from \
|
136
|
+
((select * from information_schema.table_constraints where table_name='#{table_name}') as TC \
|
137
|
+
inner join (select column_name, constraint_name from information_schema.constraint_column_usage where table_name='#{table_name}') \
|
138
|
+
as CCU on (TC.constraint_name=CCU.constraint_name) left outer join information_schema.referential_constraints REF on \
|
139
|
+
(TC.constraint_name=REF.unique_constraint_name or TC.constraint_name=REF.constraint_name) \
|
140
|
+
left join information_schema.constraint_column_usage AS CCUREF on (NOT CCUREF.table_name='#{table_name}' AND \
|
141
|
+
(REF.unique_constraint_name=CCUREF.constraint_name or REF.constraint_name=CCUREF.constraint_name)))"
|
142
|
+
|
111
143
|
results = select_all(sql, name)
|
112
144
|
constraint_name_hash = {}
|
113
145
|
results.each do |row|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
146
|
+
constraint_name = row['constraint_name']
|
147
|
+
foreign_constraint_name = row['foreign_constraint_name']
|
148
|
+
|
149
|
+
# Process constraints local to this table
|
150
|
+
if !constraint_name_hash.has_key?(constraint_name)
|
151
|
+
current_constraint = SQLServerConstraint.new(row['constraint_schema'], row['table_name'], row['column_name'], constraint_name,
|
152
|
+
row['constraint_type'], row['referenced_constraint_name'], row['foreign_table_name'], row['foreign_column_name'] )
|
153
|
+
constraints << current_constraint
|
154
|
+
constraint_name_hash[constraint_name] = current_constraint
|
155
|
+
# This key is a composite
|
156
|
+
else
|
157
|
+
current_constraint = constraint_name_hash[constraint_name]
|
158
|
+
# Unique Keys are currently the only type of composite keys supported
|
159
|
+
if current_constraint.component_of_unique_key? then current_constraint.column_name.add(row['column_name']) end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Process constraints that reference this table's local constraints
|
163
|
+
if !foreign_constraint_name.nil? && !constraint_name_hash.has_key?(foreign_constraint_name)
|
164
|
+
current_foreign_constraint = SQLServerConstraint.new(row['constraint_schema'], row['foreign_table_name'], row['foreign_column_name'],
|
165
|
+
foreign_constraint_name, SQLServerConstraint::FOREIGN_KEY_TYPE, constraint_name, row['table_name'], row['column_name'] )
|
166
|
+
constraints << current_foreign_constraint
|
167
|
+
constraint_name_hash[foreign_constraint_name] = current_foreign_constraint
|
168
|
+
# Composite FKs are currently not supported
|
169
|
+
# else
|
170
|
+
# constraint_name_hash[foreign_constraint_name].column_name.add(row['foreign_column_name'])
|
171
|
+
end
|
126
172
|
end
|
127
173
|
constraints
|
128
174
|
end
|
data/lib/drysql.rb
CHANGED
@@ -8,6 +8,9 @@ require 'connection_adapters/mysql_adapter'
|
|
8
8
|
require 'connection_adapters/postgresql_adapter'
|
9
9
|
require 'connection_adapters/sqlserver_adapter'
|
10
10
|
require 'connection_adapters/oracle_adapter'
|
11
|
+
if defined?(ActiveRecord::ConnectionAdapters::IBM_DB2Adapter)
|
12
|
+
require 'connection_adapters/ibm_db2_adapter'
|
13
|
+
end
|
11
14
|
require 'connection_adapters/abstract/schema_definitions'
|
12
15
|
require 'associations'
|
13
16
|
require 'validations'
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.11
|
|
3
3
|
specification_version: 1
|
4
4
|
name: drysql
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date: 2007-01-
|
6
|
+
version: 0.2.0
|
7
|
+
date: 2007-01-29 00:00:00 -05:00
|
8
8
|
summary: Dynamic, Reflective, Invisible Object-Relational Mapping for Ruby
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -39,6 +39,7 @@ files:
|
|
39
39
|
- lib/connection_adapters/oracle_adapter.rb
|
40
40
|
- lib/connection_adapters/sqlserver_adapter.rb
|
41
41
|
- lib/connection_adapters/abstract_adapter.rb
|
42
|
+
- lib/connection_adapters/ibm_db2_adapter.rb
|
42
43
|
- lib/connection_adapters/abstract/schema_definitions.rb
|
43
44
|
- README
|
44
45
|
test_files: []
|