mkxms-mssql 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -10
  3. data/lib/mkxms/mssql.rb +18 -0
  4. data/lib/mkxms/mssql/adoption_script_writer.rb +759 -91
  5. data/lib/mkxms/mssql/clr_aggregate_handler.rb +98 -0
  6. data/lib/mkxms/mssql/clr_assembly_handler.rb +92 -0
  7. data/lib/mkxms/mssql/clr_function_handler.rb +172 -0
  8. data/lib/mkxms/mssql/clr_impl.rb +58 -0
  9. data/lib/mkxms/mssql/clr_stored_procedure_handler.rb +88 -0
  10. data/lib/mkxms/mssql/clr_type_handler.rb +92 -0
  11. data/lib/mkxms/mssql/database_handler.rb +124 -3
  12. data/lib/mkxms/mssql/declaratives_creator.rb +206 -0
  13. data/lib/mkxms/mssql/dml_trigger_handler.rb +107 -0
  14. data/lib/mkxms/mssql/filegroup_handler.rb +1 -4
  15. data/lib/mkxms/mssql/function_handler.rb +1 -4
  16. data/lib/mkxms/mssql/indented_string_builder.rb +8 -2
  17. data/lib/mkxms/mssql/index_handler.rb +1 -4
  18. data/lib/mkxms/mssql/keywords.rb +492 -0
  19. data/lib/mkxms/mssql/primary_key_handler.rb +1 -4
  20. data/lib/mkxms/mssql/property_handler.rb +8 -0
  21. data/lib/mkxms/mssql/query_cursor.rb +12 -4
  22. data/lib/mkxms/mssql/references_handler.rb +24 -0
  23. data/lib/mkxms/mssql/role_handler.rb +1 -4
  24. data/lib/mkxms/mssql/scalar_type_handler.rb +108 -0
  25. data/lib/mkxms/mssql/schema_handler.rb +1 -4
  26. data/lib/mkxms/mssql/sql_string_manipulators.rb +4 -4
  27. data/lib/mkxms/mssql/statistics_handler.rb +1 -4
  28. data/lib/mkxms/mssql/stored_procedure_handler.rb +1 -4
  29. data/lib/mkxms/mssql/synonym_handler.rb +40 -0
  30. data/lib/mkxms/mssql/table_handler.rb +2 -8
  31. data/lib/mkxms/mssql/table_type_handler.rb +254 -0
  32. data/lib/mkxms/mssql/utils.rb +96 -0
  33. data/lib/mkxms/mssql/version.rb +1 -1
  34. data/lib/mkxms/mssql/view_handler.rb +1 -4
  35. data/spec/utils/indented_string_builder_spec.rb +21 -0
  36. data/spec/utils/query_cursor_spec.rb +2 -2
  37. data/spec/utils/sql_string_manipulators_spec.rb +59 -0
  38. metadata +18 -3
@@ -0,0 +1,92 @@
1
+ require 'mkxms/mssql/utils'
2
+
3
+ module Mkxms; end
4
+
5
+ module Mkxms::Mssql
6
+ class ClrType
7
+ include Utils::SchemaQualifiedName
8
+ include ExtendedProperties, Property::Hosting, Property::SchemaScoped
9
+
10
+ RaiserrorSource = Utils::RaiserrorWriter.new("%s: Missing or misconfigured CLR type %s")
11
+
12
+ def initialize(schema, name, assembly, clr_class)
13
+ @schema = schema
14
+ @name = name
15
+ @assembly = assembly
16
+ @clr_class = clr_class
17
+ @warning_stmt = RaiserrorSource.next_statement("WARNING".sql_quoted, qualified_name.sql_quoted, severity: :warning)
18
+ end
19
+
20
+ attr_reader :schema, :name, :warning_stmt
21
+ attr_accessor :assembly, :clr_class
22
+
23
+ def self.setup_sql
24
+ [].tap do |s|
25
+ s << "IF NOT EXISTS (SELECT * FROM sys.tables t WHERE t.object_id = OBJECT_ID(N'xmigra.ignored_clr_types'))"
26
+ s << " CREATE TABLE xmigra.ignored_clr_types ([schema] SYSNAME, name SYSNAME, CONSTRAINT PK_ignored_clr_types PRIMARY KEY ([schema], name));"
27
+
28
+ s << "" # Give a newline at the end
29
+ end.join("\n")
30
+ end
31
+
32
+ def to_sql
33
+ [].tap do |s|
34
+ s << "IF NOT EXISTS ("
35
+ s << " SELECT t.assembly_qualified_name"
36
+ s << " FROM sys.assembly_types t"
37
+ s << " JOIN sys.schemas s ON t.schema_id = s.schema_id"
38
+ s << " WHERE QUOTENAME(s.name) = #{schema.sql_quoted}"
39
+ s << " AND QUOTENAME(t.name) = #{name.sql_quoted}"
40
+ s << " UNION ALL"
41
+ s << " SELECT N''"
42
+ s << " FROM xmigra.ignored_clr_types t"
43
+ s << " WHERE t.[schema] = #{schema.sql_quoted}"
44
+ s << " AND t.name = #{name.sql_quoted}"
45
+ s << ") BEGIN"
46
+ s << " CREATE TYPE #{schema}.#{name} EXTERNAL NAME #{assembly}.#{clr_class};"
47
+ s.concat(extended_properties_sql.map {|s| " " + s})
48
+ s << "END"
49
+
50
+ s << "IF NOT EXISTS ("
51
+ s << " SELECT CONCAT(s.name, N'.', t.name) as clr_type, QUOTENAME(asm.name) as assembly, QUOTENAME(t.assembly_class) as clr_class"
52
+ s << " FROM sys.assembly_types t"
53
+ s << " JOIN sys.schemas s ON t.schema_id = s.schema_id"
54
+ s << " JOIN sys.assemblies asm ON t.assembly_id = asm.assembly_id"
55
+ s << " WHERE QUOTENAME(s.name) = #{schema.sql_quoted}"
56
+ s << " AND QUOTENAME(t.name) = #{name.sql_quoted}"
57
+ s << " -- #{warning_stmt.error_marker} Run the query up to this point for CLR type configuration --"
58
+ cols = [
59
+ ["assembly", assembly],
60
+ ["clr_class", clr_class],
61
+ ].map {|t, v| [t.ljust(v.length), v.ljust(t.length)]}
62
+ s << (" -- " + cols.map {|e| e[0]}.join(' ') + ' --')
63
+ s << (" -- Expected values: " + cols.map {|e| e[1]}.join(' ') + ' --')
64
+ s << " AND QUOTENAME(asm.name) = #{assembly.sql_quoted}"
65
+ s << " AND QUOTENAME(t.assembly_class) = #{clr_class.sql_quoted}"
66
+ s << " UNION ALL"
67
+ s << " SELECT CONCAT(t.[schema], N'.', t.name), NULL, NULL"
68
+ s << " FROM xmigra.ignored_clr_types t"
69
+ s << " WHERE t.[schema] = #{schema.sql_quoted}"
70
+ s << " AND t.name = #{name.sql_quoted}"
71
+ s << ") #{warning_stmt};"
72
+
73
+ s << "" # Give a newline at the end
74
+ end
75
+ end
76
+ end
77
+
78
+ class ClrTypeHandler
79
+ include PropertyHandler::ElementHandler
80
+
81
+ def initialize(types, node)
82
+ a = node.attributes
83
+
84
+ @type_info = ClrType.new(
85
+ a['schema'],
86
+ a['name'],
87
+ a['assembly'],
88
+ a['class']
89
+ ).tap {|t| types << store_properties_on(t)}
90
+ end
91
+ end
92
+ end
@@ -7,7 +7,13 @@ require 'yaml'
7
7
 
8
8
  adoption_script_writer
9
9
  check_constraint_handler
10
+ clr_aggregate_handler
11
+ clr_assembly_handler
12
+ clr_function_handler
13
+ clr_stored_procedure_handler
14
+ clr_type_handler
10
15
  default_constraint_handler
16
+ dml_trigger_handler
11
17
  filegroup_handler
12
18
  foreign_key_handler
13
19
  function_handler
@@ -16,10 +22,13 @@ require 'yaml'
16
22
  primary_key_handler
17
23
  property_handler
18
24
  role_handler
25
+ scalar_type_handler
19
26
  schema_handler
20
27
  statistics_handler
21
28
  stored_procedure_handler
29
+ synonym_handler
22
30
  table_handler
31
+ table_type_handler
23
32
  unique_constraint_handler
24
33
  utils
25
34
  view_handler
@@ -34,15 +43,32 @@ module Mkxms::Mssql
34
43
  include ExtendedProperties, PropertyHandler::ElementHandler
35
44
 
36
45
  ADOPTION_SQL_FILE = "adopt.sql"
46
+ DRY_RUN_MARKER = "for dry run"
47
+
48
+ class IgnoreText
49
+ def initialize(node)
50
+ end
51
+
52
+ def handle_text(t, node)
53
+ end
54
+ end
37
55
 
38
56
  def initialize(**kwargs)
39
57
  @schema_dir = kwargs[:schema_dir] || Pathname.pwd
40
58
  end
41
59
 
42
60
  attr_reader :schema_dir
43
- attr_init(:filegroups, :schemas, :roles, :tables, :column_defaults, :pku_constraints, :foreign_keys, :check_constraints){[]}
61
+ attr_init(
62
+ :filegroups, :schemas, :roles,
63
+ :types,
64
+ :clr_assemblies, :clr_types,
65
+ :tables,
66
+ :column_defaults, :pku_constraints, :foreign_keys,
67
+ :check_constraints, :dml_triggers,
68
+ :synonyms,
69
+ ){[]}
44
70
  attr_init(:indexes, :statistics){[]}
45
- attr_init(:views, :udfs, :procedures){[]}
71
+ attr_init(:views, :udfs, :procedures, :aggregates){[]}
46
72
  attr_init(:permissions){[]}
47
73
 
48
74
  def handle_database_element(parse)
@@ -60,6 +86,14 @@ module Mkxms::Mssql
60
86
  parse.delegate_to SchemaHandler, schemas
61
87
  end
62
88
 
89
+ def handle_type_element(parse)
90
+ parse.delegate_to ScalarTypeHandler, types
91
+ end
92
+
93
+ def handle_table_type_element(parse)
94
+ parse.delegate_to TableTypeHandler, types
95
+ end
96
+
63
97
  def handle_role_element(parse)
64
98
  parse.delegate_to RoleHandler, roles
65
99
  end
@@ -104,10 +138,22 @@ module Mkxms::Mssql
104
138
  parse.delegate_to StoredProcedureHandler, procedures
105
139
  end
106
140
 
141
+ def handle_clr_stored_procedure_element(parse)
142
+ parse.delegate_to ClrStoredProcedureHandler, procedures
143
+ end
144
+
107
145
  def handle_user_defined_function_element(parse)
108
146
  parse.delegate_to FunctionHandler, udfs
109
147
  end
110
148
 
149
+ def handle_clr_function_element(parse)
150
+ parse.delegate_to ClrFunctionHandler, udfs
151
+ end
152
+
153
+ def handle_clr_aggregate_element(parse)
154
+ parse.delegate_to ClrArggregateHandler, aggregates
155
+ end
156
+
111
157
  def handle_granted_element(parse)
112
158
  parse.delegate_to PermissionHandler, permissions
113
159
  end
@@ -116,11 +162,32 @@ module Mkxms::Mssql
116
162
  parse.delegate_to PermissionHandler, permissions
117
163
  end
118
164
 
165
+ def handle_clr_assembly_element(parse)
166
+ parse.delegate_to ClrAssemblyHandler, clr_assemblies
167
+ end
168
+
169
+ def handle_clr_type_element(parse)
170
+ parse.delegate_to ClrTypeHandler, clr_types
171
+ end
172
+
173
+ def handle_dml_trigger_element(parse)
174
+ parse.delegate_to DmlTriggerHandler, dml_triggers
175
+ end
176
+
177
+ def handle_synonym_element(parse)
178
+ parse.delegate_to SynonymHandler, synonyms
179
+ end
180
+
119
181
  def create_source_files
120
182
  dbinfo_path = @schema_dir.join(XMigra::SchemaManipulator::DBINFO_FILE)
121
183
 
122
184
  if dbinfo_path.exist?
123
- raise ProgramArgumentError.new("#{@schema_dir} already contains an XMigra schema")
185
+ if dbinfo_path.open {|f| YAML.load(f)[DRY_RUN_MARKER]}
186
+ # Delete everything in the source files, so we can do a dry run over
187
+ @schema_dir.each_child {|e| e.rmtree}
188
+ else
189
+ raise ProgramArgumentError.new("#{@schema_dir} already contains an XMigra schema")
190
+ end
124
191
  end
125
192
 
126
193
  # TODO: Sort dependencies of triggers, views, user defined functions, and
@@ -133,10 +200,21 @@ module Mkxms::Mssql
133
200
  # Create and populate @schema_dir + XMigra::SchemaManipulator::DBINFO_FILE
134
201
  dbinfo_path.open('w') do |dbi|
135
202
  dbi.puts "system: #{XMigra::MSSQLSpecifics::SYSTEM_NAME}"
203
+ if Utils.dry_run?
204
+ dbi.puts "#{DRY_RUN_MARKER}: true"
205
+ end
136
206
  end
137
207
 
138
208
  # TODO: Create migration to check required filegroups and files
139
209
 
210
+ # Migration: Check CLR assemblies
211
+ create_migration(
212
+ "check-clr-assemblies",
213
+ "Check expected CLR assemblies have been created.",
214
+ ClrAssembly.setup_sql + "\n" + joined_modobj_sql(clr_assemblies),
215
+ clr_assemblies.map(&:name).sort
216
+ )
217
+
140
218
  # Migration: Create roles
141
219
  create_migration(
142
220
  "create-roles",
@@ -153,6 +231,30 @@ module Mkxms::Mssql
153
231
  schemas.map(&:name).sort
154
232
  )
155
233
 
234
+ # Migration: Create scalar types
235
+ create_migration(
236
+ "create-scalar-types",
237
+ "Create user-defined scalar types.",
238
+ joined_modobj_sql(types),
239
+ types.map {|t| [t.schema, t.qualified_name]}.flatten.uniq.sort
240
+ )
241
+
242
+ # Migration: Create synonyms
243
+ create_migration(
244
+ "create-synonyms",
245
+ "Create synonyms for other objects in the database.",
246
+ joined_modobj_sql(synonyms),
247
+ synonyms.map {|s| [s.schema, s.qualified_name]}.flatten
248
+ )
249
+
250
+ # Migration: Create CLR types that don't exist
251
+ create_migration(
252
+ "create-clr-types",
253
+ "Create CLR types (unless already existing).",
254
+ ClrType.setup_sql + "\n" + joined_modobj_sql(clr_types),
255
+ clr_types.map(&:qualified_name).sort
256
+ )
257
+
156
258
  tables.each do |table|
157
259
  # Migration: Create table
158
260
  qual_name = [table.schema, table.name].join('.')
@@ -196,6 +298,16 @@ module Mkxms::Mssql
196
298
  check_constraints.map {|c| [c.schema, c.qualified_table, c.qualified_name].compact}.flatten.uniq.sort
197
299
  )
198
300
 
301
+ # Migration: Add DML triggers
302
+ create_migration(
303
+ "add-triggers",
304
+ "Add triggers.",
305
+ joined_modobj_sql(dml_triggers, sep: DmlTriggerHandler.ddl_block_separator) + "\n",
306
+ dml_triggers.map do |t|
307
+ [t.schema, t.table.qualified_name, t.qualified_name].compact
308
+ end.flatten.uniq.sort
309
+ ) unless dml_triggers.empty?
310
+
199
311
  # Check that no super-permissions reference a view, user-defined function, or stored procedure
200
312
  access_object_names = (views + udfs + procedures).map {|ao| ao.qualified_name}
201
313
  permissions.map {|p| p.super_permissions}.flatten.select do |p|
@@ -228,6 +340,15 @@ module Mkxms::Mssql
228
340
 
229
341
  write_statistics
230
342
 
343
+ aggregates.each do |agg|
344
+ create_migration(
345
+ "register-#{agg.qualified_name}-aggregate",
346
+ "Register the CLR aggregate function #{agg.qualified_name}",
347
+ agg.to_sql.join("\nGO\n"),
348
+ [agg.schema, agg.qualified_name]
349
+ )
350
+ end
351
+
231
352
  views.each do |view|
232
353
  write_access_def(view, 'view')
233
354
  end
@@ -0,0 +1,206 @@
1
+ require 'pathname'
2
+ require 'psych' # YAML support
3
+ require 'xmigra'
4
+ require 'mkxms/mssql/keywords'
5
+
6
+ module Mkxms; end
7
+
8
+ module Mkxms::Mssql
9
+ class DeclarativesCreator
10
+ def initialize(document, schema_dir)
11
+ @document = document
12
+ @schema_dir = schema_dir || Pathname.pwd
13
+ end
14
+
15
+ def decls_dir
16
+ @schema_dir.join(
17
+ XMigra::SchemaManipulator::STRUCTURE_SUBDIR,
18
+ XMigra::DeclarativeMigration::SUBDIR,
19
+ )
20
+ end
21
+
22
+ def create_artifacts
23
+ index_constraints
24
+
25
+ # Loop through all tables
26
+ decl_paths = []
27
+ @document.elements.each('/database/table') do |table|
28
+ schema, name = %w[schema name].map {|a| table.attributes[a]}
29
+ tdecl_path = decls_dir.join([schema, name, 'yaml'].join('.'))
30
+ doc = build_declarative(table)
31
+ decls_dir.mkpath
32
+ tdecl_path.open('w') {|f| f.write(doc.to_yaml)}
33
+ decl_paths << tdecl_path
34
+ end
35
+
36
+ # Loop through the created paths creating an adoption migration for each
37
+ decl_paths.each do |fpath|
38
+ tool = XMigra::ImpdeclMigrationAdder.new(@schema_dir)
39
+ tool.add_migration_implementing_changes(fpath, {adopt: true})
40
+ end
41
+ end
42
+
43
+ def build_declarative(table)
44
+ doc, tdecl = create_blank_table_decl
45
+
46
+ columns_decl = Psych::Nodes::Sequence.new.tap do |s|
47
+ tdecl.children << node_from('columns') << s
48
+ end
49
+
50
+ table_key = %w[schema name].map {|a| table.attributes[a]}
51
+
52
+ # Columns (including single-column default constraints)
53
+ table.elements.each('column') do |column|
54
+ entry = Psych::Nodes::Mapping.new.tap {|e| columns_decl.children << e}
55
+ col_name = column.attributes['name']
56
+ if col_name =~ /^\[[a-zA-Z_][a-zA-Z0-9_]*\]$/ && !KEYWORDS_SET.include?(col_name[1..-2].upcase)
57
+ col_name = col_name[1..-2]
58
+ end
59
+ entry.children.concat(
60
+ ['name', col_name].map {|v| node_from(v)}
61
+ )
62
+ col_type = column.attributes['type']
63
+ if KEYWORDS_SET.include?(basic_type = col_type[1..-2].upcase)
64
+ col_type = basic_type
65
+ end
66
+ if capacity = column.attributes['capacity']
67
+ col_type = "#{col_type}(#{capacity})"
68
+ end
69
+ entry.children.concat(
70
+ ['type', col_type].map {|v| node_from(v)}
71
+ )
72
+ unless column.attributes['nullable']
73
+ entry.children.concat(
74
+ ['nullable', false].map {|v| node_from(v)}
75
+ )
76
+ end
77
+
78
+ if cstr = cstr_on_column(@default_constraints, table_key, column)
79
+ entry.children.concat(['default', cstr.text].map {|v| node_from(v)})
80
+ end
81
+ if cexpr = column.elements['computed-expression']
82
+ entry.children.concat(['X-computed-as', cexpr.text].map {|v| node_from(v)})
83
+ end
84
+ end
85
+
86
+ # Everything but default constraints
87
+ cstrs_decl = Psych::Nodes::Mapping.new
88
+ constraint_default_name_part = mashable_name(table.attributes['name'])
89
+ @primary_key_constraints.fetch(table_key, []).each do |cstr|
90
+ cstr_name = cstr.attributes['name'] || "PK_#{constraint_default_name_part}"
91
+ cstrs_decl.children << node_from(cstr_name) << node_from({
92
+ 'type' => 'primary key',
93
+ 'columns' => cstr.elements.enum_for(:each, 'column').map {|c| c.attributes['name']},
94
+ })
95
+ end
96
+ @uniqueness_constraints.fetch(table_key, []).each do |cstr|
97
+ cstr_name = cstr.attributes['name'] || (
98
+ "UQ_#{constraint_default_name_part}_" +
99
+ mashable_name(
100
+ cstr.elements.enum_for(:each, 'column').map {|c| c.attributes['name']}.join('_')
101
+ )
102
+ )
103
+ cstrs_decl.children << node_from(cstr_name) << node_from({
104
+ 'type' => 'unique',
105
+ 'columns' => cstr.elements.enum_for(:each, 'column').map {|c| c.attributes['name']},
106
+ })
107
+ end
108
+ @foreign_key_constraints.fetch(table_key, []).each do |cstr|
109
+ cstr_name = cstr.attributes['name'] || :generated
110
+ if cstr_name == :generated
111
+ from_cols, to_cols = [], []
112
+ cstr.elements.each('link') do |link|
113
+ from_cols << link.attributes['from']
114
+ to_cols << link.attributes['to']
115
+ end
116
+ cstr_name = (
117
+ "FK_#{constraint_default_name_part}_" +
118
+ mashable_name(from_cols.join('_')) + '_' +
119
+ mashable_name(%w[schema name].map {|a| cstr.elements['referent'].attributes[a]}.join('_')) + '_' +
120
+ mashable_name(to_cols.join('_'))
121
+ )
122
+ end
123
+ cstrs_decl.children << node_from(cstr_name) << node_from({
124
+ 'link to' => cstr.elements['referent'].tap do |r|
125
+ break [r.attributes['schema'], r.attributes['name']].join('.')
126
+ end,
127
+ 'columns' => Hash[
128
+ cstr.elements.enum_for(:each, 'link').map do |link|
129
+ %w[from to].map {|a| link.attributes[a]}
130
+ end
131
+ ],
132
+ })
133
+ end
134
+ existing_check_names = nil
135
+ @check_constraints.fetch(table_key, []).each_with_index do |cstr, i|
136
+ cstr_name = cstr.attributes['name'] || :generated
137
+ if cstr_name == :generated
138
+ existing_check_names ||= @check_constraints[table_key].map {|c| c.attributes['name']}.compact
139
+ cstr_name = "CK_#{constraint_default_name_part}_#{i+1}"
140
+ while existing_check_names.include?(cstr_name)
141
+ cstr_name << '_' unless cstr_name.end_with?('_')
142
+ cstr_name << 'X'
143
+ end
144
+ existing_check_names << cstr_name
145
+ end
146
+ cstrs_decl.children << node_from(cstr_name) << node_from({
147
+ 'verify' => cstr.text,
148
+ })
149
+ end
150
+
151
+ unless cstrs_decl.children.empty?
152
+ tdecl.children << node_from("constraints") << cstrs_decl
153
+ end
154
+
155
+ return doc
156
+ end
157
+
158
+ def index_constraints
159
+ @primary_key_constraints = read_constraints('primary-key')
160
+ @uniqueness_constraints = read_constraints('unique-constraint')
161
+ @foreign_key_constraints = read_constraints('foreign-key')
162
+ @check_constraints = read_constraints('check-constraint')
163
+ @default_constraints = read_constraints('default-constraint')
164
+ end
165
+
166
+ def read_constraints(ctype, inline: false)
167
+ @document.elements.enum_for(:each, "/database/#{ctype}").each_with_object({}) do |cstr, result|
168
+ key = [cstr.attributes['schema'], cstr.attributes['table']]
169
+ (result[key] ||= []) << cstr
170
+ end
171
+ end
172
+
173
+ def create_blank_table_decl
174
+ stream = Psych::Nodes::Stream.new
175
+ doc = Psych::Nodes::Document.new.tap {|d| stream.children << d}
176
+ decl = Psych::Nodes::Mapping.new.tap {|m| doc.children << m}
177
+ decl.implicit = false
178
+ decl.tag = '!table'
179
+ return [stream, decl]
180
+ end
181
+
182
+ def node_from(val)
183
+ ast_stream = Psych.parse_stream(Psych.dump(val))
184
+ return ast_stream.children[0].children[0]
185
+ end
186
+
187
+ def attr_eq?(a, o1=nil, *objs)
188
+ return true if o1.nil? || objs.length == 0
189
+ val = o1.attributes[a]
190
+ return objs.all? {|o| o.attributes[a] == val}
191
+ end
192
+
193
+ def cstr_on_column(group, key, column)
194
+ cstrs = group[key]
195
+ return nil unless cstrs
196
+ cstrs.find do |cstr|
197
+ cstr.attributes['column'] == column.attributes['name'] || \
198
+ cstr.elements.enum_for(:each, 'column').select {|c| attr_eq?('name', c, column)}.count > 0
199
+ end
200
+ end
201
+
202
+ def mashable_name(s)
203
+ s.gsub(/[\]\[]/, '').gsub(/[^a-zA-Z_]/, '_')
204
+ end
205
+ end
206
+ end