mkxms-mssql 1.0.0 → 1.1.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.
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