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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b79eb5cd26ad73ff11edc3d488344e569a723439
4
- data.tar.gz: d2dd1a1381d3d715b2dd5dfb066c7aed4f7f4b6e
3
+ metadata.gz: acbe79b89bb652174600791fc2290a37d6bbd19a
4
+ data.tar.gz: a1d4a0b1871fd80f0bb28e7d8eeed9f1b36f626d
5
5
  SHA512:
6
- metadata.gz: 9ad174edd9c865f2c5687567dd98a7a66bd38559ba0fc28715768d4dd03dc24d0830d8ff6baa5e214f6f7c97682bdd7e92eb9f206f7a14ecf1e1fb89b5ddc493
7
- data.tar.gz: 1fa9637d1da5cc828ef1d67f886144a98f6143433e5802a36af73117d03ed1c9b3e30b06da95edd8b3db567340a7d810aebb9ad1bd0dfb082863874e4a4bd284
6
+ metadata.gz: 39a7e7fc9bfefa49985c3625699194e940741d0475d8279b8629af38cee368cd980069c78a0bdeed35b8d9275920374a751c63b643d8f1e87ce649581f69001f
7
+ data.tar.gz: bc8bc5753d47aacb298fb3d76887fe607095d3b75b5e6c602eee6add90d2e1879643d81108371a643a4a8b38022ca25b93f4609341f6302b8f1eb1d9b8c745f4
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # Mkxms::Mssql
1
+ # Mkxms::Mssql [![Gem Version](https://badge.fury.io/rb/mkxms-mssql.svg)](http://badge.fury.io/rb/mkxms-mssql)
2
2
 
3
3
  Creates a set of XMigra (https://rubygems.org/gems/xmigra) source files
4
4
  from a database description XML document (as generated by
5
- [mssql-eyewkas.sql](https://gist.github.com/rtweeks/62d8fb9c6ca3de1195d9#file-mssql-eyewkas-sql)
5
+ [mssql-eyewkas.sql](https://gist.github.com/rtweeks/62d8fb9c6ca3de1195d9/9d38f51f5f02a65e6146875c15714f9138a3b86c)
6
6
  or a compatible later version).
7
7
 
8
8
  ## Installation
@@ -18,22 +18,16 @@ Run `mkxms-mssql [-o DEST_DIR] [DB_DESCRIPTION_XML_FILE]`
18
18
  ## Project Status
19
19
 
20
20
  The 1.x series of releases is intended to incrementally include support for
21
- additional Microsoft SQL Server features. As of version 1.0, the following
21
+ additional Microsoft SQL Server features. As of version 1.1, the following
22
22
  features are NOT supported by this program (although they may be supported
23
23
  by XMigra):
24
24
 
25
25
  * Partition functions and partition schemes
26
- * CLR assemblies
27
- * Synonyms
28
26
  * XML schema collections
29
- * User-defined (SQL) types - scalar or table
30
- * CLR types
31
27
  * Fulltext indexes
32
28
  * Rules
33
- * Triggers
29
+ * DDL Triggers
34
30
  * Spatial indexes
35
- * CLR stored procedures
36
- * CLR functions
37
31
  * Service Broker configuration
38
32
 
39
33
  Any elements in the database description XML relating to the features above
data/lib/mkxms/mssql.rb CHANGED
@@ -16,6 +16,11 @@ module Mkxms
16
16
  flags.on('-o', '--outdir=SCHEMA_DIR', "Output in SCHEMA_DIR") do |schema_dir|
17
17
  options.schema_dir = Pathname(schema_dir).expand_path
18
18
  end
19
+
20
+ options.generate_declaratives = true
21
+ flags.on('--[no-]declaratives', "Generate declarative support files") do |v|
22
+ options.generate_declaratives = v
23
+ end
19
24
  end
20
25
 
21
26
  db_files = optparser.parse(argv)
@@ -32,6 +37,16 @@ module Mkxms
32
37
  engine = Engine.new(document, db_handler)
33
38
  engine.run
34
39
  db_handler.create_source_files
40
+ if generate_declaratives_indicated(options)
41
+ DeclarativesCreator.new(document, options[:schema_dir]).create_artifacts
42
+ end
43
+ end
44
+
45
+ def self.generate_declaratives_indicated(options)
46
+ options[:generate_declaratives].tap do |val|
47
+ return val unless val.nil?
48
+ end
49
+ return Gem::Version.new(XMigra::VERSION) >= Gem::Version.new("1.6.0")
35
50
  end
36
51
 
37
52
  def self.with_db_description_io(file_path, &blk)
@@ -53,9 +68,12 @@ module Mkxms
53
68
  end
54
69
 
55
70
  require "mkxms/mssql/database_handler"
71
+ require "mkxms/mssql/declaratives_creator"
56
72
  require "mkxms/mssql/engine"
57
73
  require "mkxms/mssql/exceptions"
58
74
  require "mkxms/mssql/version"
75
+ require "rubygems"
76
+ require "xmigra"
59
77
 
60
78
  if __FILE__.eql? $0
61
79
  Mkxms::Mssql.run_program
@@ -3,6 +3,7 @@ require 'xmigra'
3
3
  require 'mkxms/mssql/indented_string_builder'
4
4
  require 'mkxms/mssql/query_cursor'
5
5
  require 'mkxms/mssql/sql_string_manipulators'
6
+ require 'mkxms/mssql/utils'
6
7
 
7
8
  module Mkxms; end
8
9
 
@@ -20,13 +21,17 @@ module Mkxms::Mssql
20
21
  attr_reader :db_expectations
21
22
 
22
23
  def create_script(path)
24
+ if Utils.dry_run?
25
+ base, _, ext = path.to_s.rpartition('.')
26
+ path = [base, 'dry-run', ext].join('.')
27
+ end
23
28
  Pathname(path).open('w') do |script|
24
29
  script.puts adoption_sql
25
30
  end
26
31
  end
27
32
 
28
33
  def adoption_sql
29
- in_ddl_transaction do
34
+ in_ddl_transaction(dry_run: Utils.dry_run?) do
30
35
  script_parts = [
31
36
  # Check for blatantly incorrect application of script, e.g. running
32
37
  # on master or template database.
@@ -39,6 +44,9 @@ module Mkxms::Mssql
39
44
  # Create an error table
40
45
  :create_adoption_error_table_sql,
41
46
 
47
+ # Check CLR assemblies
48
+ :check_clr_assemblies,
49
+
42
50
  # Check roles
43
51
  :check_expected_roles_exist_sql,
44
52
  :check_expected_role_membership_sql,
@@ -46,6 +54,15 @@ module Mkxms::Mssql
46
54
  # Check schemas
47
55
  :check_expected_schemas_exist_sql,
48
56
 
57
+ # Check user-defined types
58
+ :check_user_defined_types_sql,
59
+
60
+ # Check CLR types
61
+ :check_clr_types,
62
+
63
+ # Check CLR aggregates
64
+ :check_clr_aggregates,
65
+
49
66
  # Check tables (including columns)
50
67
  :check_tables_exist_and_structured_as_expected_sql,
51
68
 
@@ -61,6 +78,9 @@ module Mkxms::Mssql
61
78
  # Check check constraints
62
79
  :check_check_constraints_sql,
63
80
 
81
+ # Check DML triggers
82
+ :check_dml_triggers,
83
+
64
84
  # Adopt indexes
65
85
  :adopt_indexes_sql,
66
86
 
@@ -109,7 +129,7 @@ module Mkxms::Mssql
109
129
  )
110
130
  BEGIN
111
131
  DROP TABLE [xmigra].[adoption_errors];
112
- END;
132
+ END
113
133
  GO
114
134
 
115
135
  CREATE TABLE [xmigra].[adoption_errors] (
@@ -130,13 +150,55 @@ module Mkxms::Mssql
130
150
  BEGIN
131
151
  SELECT * FROM [xmigra].[adoption_errors];
132
152
  RAISERROR (N'Database adoption failed.', 11, 1);
133
- END;
153
+ END
134
154
 
135
155
  DROP TABLE [xmigra].[adoption_errors];
136
156
  }
137
157
  end
138
158
  end
139
159
 
160
+ def check_clr_assemblies
161
+ db_expectations.clr_assemblies.map do |asm|
162
+ dedent %Q{
163
+ IF NOT EXISTS (
164
+ SELECT * FROM sys.assemblies asm
165
+ WHERE asm.is_visible = 1 AND QUOTENAME(asm.name) = #{asm.name.sql_quoted}
166
+ )
167
+ BEGIN
168
+ #{adoption_error_sql "CLR assembly #{asm.name} does not exist."}
169
+ END
170
+
171
+ IF NOT EXISTS (
172
+ SELECT * FROM sys.assemblies asm
173
+ JOIN sys.database_principals owner ON asm.principal_id = owner.principal_id
174
+ WHERE asm.is_visible = 1 AND QUOTENAME(asm.name) = #{asm.name.sql_quoted}
175
+ AND QUOTENAME(owner.name) = #{asm.owner.sql_quoted}
176
+ )
177
+ BEGIN
178
+ #{adoption_error_sql "CLR assembly #{asm.name} should be owned by #{asm.owner}"}
179
+ END
180
+
181
+ IF NOT EXISTS (
182
+ SELECT * FROM sys.assemblies asm
183
+ WHERE asm.is_visible = 1 AND QUOTENAME(asm.name) = #{asm.name.sql_quoted}
184
+ AND REPLACE(LOWER(asm.permission_set_desc), '_', '-') = #{asm.access.sql_quoted}
185
+ )
186
+ BEGIN
187
+ #{adoption_error_sql "CLR assembly #{asm.name} should have permission set #{asm.access}"}
188
+ END
189
+
190
+ IF NOT EXISTS (
191
+ SELECT * FROM sys.assemblies asm
192
+ WHERE asm.is_visible = 1 AND QUOTENAME(asm.name) = #{asm.name.sql_quoted}
193
+ AND asm.clr_name = #{asm.lib_name.sql_quoted}
194
+ )
195
+ BEGIN
196
+ #{adoption_error_sql %Q{CLR assembly #{asm.name} should reference library "#{asm.lib_name}".}}
197
+ END
198
+ }
199
+ end
200
+ end
201
+
140
202
  def check_expected_roles_exist_sql
141
203
  db_expectations.roles.map do |r|
142
204
  dedent %Q{
@@ -147,7 +209,7 @@ module Mkxms::Mssql
147
209
  )
148
210
  BEGIN
149
211
  #{adoption_error_sql "Role #{r.name} does not exist."}
150
- END;
212
+ END
151
213
 
152
214
  IF EXISTS (
153
215
  SELECT * FROM sys.database_principals r
@@ -157,7 +219,7 @@ module Mkxms::Mssql
157
219
  )
158
220
  BEGIN
159
221
  #{adoption_error_sql "Role #{r.name} should be owned by #{r.owner}."}
160
- END;
222
+ END
161
223
  }
162
224
  end.join("\n")
163
225
  end
@@ -176,7 +238,7 @@ module Mkxms::Mssql
176
238
  )
177
239
  BEGIN
178
240
  #{adoption_error_sql "Role #{r.name} should be a member of #{er_name}."}
179
- END;
241
+ END
180
242
  })
181
243
  end
182
244
  end
@@ -200,7 +262,326 @@ module Mkxms::Mssql
200
262
  )
201
263
  BEGIN
202
264
  #{adoption_error_sql "Schema #{schema.name} is not owned by #{schema.owner}."}
203
- END;
265
+ END
266
+ }
267
+ end
268
+ end
269
+
270
+ def check_user_defined_types_sql
271
+ db_expectations.types.map do |t|
272
+ dedent %Q{
273
+ IF NOT EXISTS (
274
+ SELECT * FROM sys.types t
275
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
276
+ WHERE QUOTENAME(s.name) = #{t.schema.sql_quoted}
277
+ AND QUOTENAME(t.name) = #{t.name.sql_quoted}
278
+ )
279
+ BEGIN
280
+ #{adoption_error_sql "User-defined scalar type #{t.qualified_name} is not defined."}
281
+ END
282
+
283
+ IF NOT EXISTS (
284
+ SELECT * FROM sys.types t
285
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
286
+ WHERE t.is_user_defined <> 0
287
+ AND t.is_assembly_type = 0
288
+ AND t.user_type_id NOT IN (
289
+ SELECT st.system_type_id
290
+ FROM sys.types st
291
+ )
292
+ AND QUOTENAME(s.name) = #{t.schema.sql_quoted}
293
+ AND QUOTENAME(t.name) = #{t.name.sql_quoted}
294
+ )
295
+ BEGIN
296
+ #{adoption_error_sql "#{t.qualified_name} is not a user-defined type."}
297
+ END
298
+
299
+ } + (
300
+ if t.respond_to?(:columns)
301
+ check_table_type_components_sql(t)
302
+ else
303
+ dedent %Q{
304
+ IF NOT EXISTS (
305
+ SELECT * FROM sys.types t
306
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
307
+ JOIN sys.types bt ON t.system_type_id = bt.user_type_id
308
+ WHERE t.is_user_defined <> 0
309
+ AND QUOTENAME(s.name) = #{t.schema.sql_quoted}
310
+ AND QUOTENAME(t.name) = #{t.name.sql_quoted}
311
+ AND QUOTENAME(bt.name) = #{t.base_type.sql_quoted}
312
+ AND t.is_nullable #{t.nullable? ? "<>" : "="} 0
313
+ #{
314
+ case t.capacity
315
+ when 'max'
316
+ "AND t.max_length = -1"
317
+ when Integer
318
+ "AND t.max_length = #{t.element_size * t.capacity}"
319
+ end
320
+ }
321
+ #{"AND t.precision = #{t.precision}" if t.precision}
322
+ #{"AND t.scale = #{t.scale}" if t.scale}
323
+ )
324
+ BEGIN
325
+ #{adoption_error_sql "#{t.qualified_name} is not defined as #{t.type_spec}."}
326
+ END
327
+ }
328
+ end
329
+ )
330
+ end
331
+ end
332
+
333
+ class TableTypeKeyConstraintChecks < IndentedStringBuilder
334
+ include SqlStringManipulators
335
+ extend SqlStringManipulators
336
+
337
+ def initialize(table, constraint, error_sql_proc)
338
+ super()
339
+
340
+ @table = table
341
+ @constraint = constraint
342
+ @error_sql_proc = error_sql_proc
343
+
344
+ add_tests
345
+ end
346
+
347
+ attr_reader :table, :constraint
348
+
349
+ def error_sql(s)
350
+ @error_sql_proc.call(s)
351
+ end
352
+
353
+ def add_tests
354
+ dsl {
355
+ puts "IF NOT EXISTS(%s)" do
356
+ puts dedent %Q{
357
+ SELECT * FROM sys.key_constraints kc
358
+ JOIN sys.table_types tt ON tt.type_table_object_id = kc.parent_object_id
359
+ WHERE tt.user_type_id = TYPE_ID(#{table.qualified_name.sql_quoted})
360
+ AND kc.type = #{
361
+ case constraint.type
362
+ when 'PRIMARY KEY' then 'PK'
363
+ when 'UNIQUE' then 'UQ'
364
+ else raise "Unknown key constraint type"
365
+ end.sql_quoted
366
+ }
367
+ }
368
+ constraint.columns.each_with_index do |col, i|
369
+ puts dedent %Q{
370
+ AND (
371
+ SELECT ic.key_ordinal
372
+ FROM sys.index_columns ic
373
+ JOIN sys.columns c
374
+ ON c.object_id = ic.object_id
375
+ AND c.column_id = ic.column_id
376
+ WHERE ic.object_id = kc.parent_object_id
377
+ AND ic.index_id = kc.unique_index_id
378
+ AND QUOTENAME(c.name) = #{col.name.sql_quoted}
379
+ ) = #{i + 1}
380
+ }
381
+ end
382
+ end
383
+ puts "BEGIN"..."END" do
384
+ puts error_sql "Table type #{table.qualified_name} does not have a #{constraint.type} constraint with the expected sequence of columns (#{constraint.columns.map(&:name).join(', ')})."
385
+ end
386
+ }
387
+ end
388
+ end
389
+
390
+ def check_table_type_components_sql(t)
391
+ [].tap do |tests|
392
+ t.columns.each do |col|
393
+ # The column itself
394
+ tests << (dedent %Q{
395
+ IF NOT EXISTS (
396
+ SELECT * FROM sys.columns c
397
+ JOIN sys.table_types tt ON tt.type_table_object_id = c.object_id
398
+ JOIN sys.schemas ts ON ts.schema_id = tt.schema_id
399
+ JOIN sys.types ct ON ct.user_type_id = c.user_type_id
400
+ JOIN sys.schemas cts ON cts.schema_id = ct.schema_id
401
+ WHERE QUOTENAME(c.name) = #{col.name.sql_quoted}
402
+ AND QUOTENAME(cts.name) = #{(col.type_schema || "[sys]").sql_quoted}
403
+ AND QUOTENAME(ct.name) = #{col.type_name.sql_quoted}
404
+ )
405
+ BEGIN
406
+ #{adoption_error_sql "Table type #{t.qualified_name} does not have a column named #{col.name}."}
407
+ END
408
+
409
+ IF NOT EXISTS (
410
+ SELECT * FROM sys.columns c
411
+ JOIN sys.table_types tt ON tt.type_table_object_id = c.object_id
412
+ JOIN sys.schemas ts ON ts.schema_id = tt.schema_id
413
+ JOIN sys.types ct ON ct.user_type_id = c.user_type_id
414
+ JOIN sys.schemas cts ON cts.schema_id = ct.schema_id
415
+ WHERE QUOTENAME(c.name) = #{col.name.sql_quoted}
416
+ AND QUOTENAME(cts.name) = #{(col.type_schema || "[sys]").sql_quoted}
417
+ AND QUOTENAME(ct.name) = #{col.type_name.sql_quoted}
418
+ AND c.is_nullable = #{col.nullable? ? 1 : 0}
419
+ #{"AND c.max_length = #{col.max_byte_consumption}" if col.capacity}
420
+ #{"AND c.collation_name = #{col.collation.sql_quoted}" if col.collation}
421
+ #{"AND c.precision = #{col.precision}" if col.precision}
422
+ #{"AND c.scale = #{col.scale}" if col.scale}
423
+ )
424
+ BEGIN
425
+ #{adoption_error_sql "Column #{col.name} of #{t.qualified_name} is not defined as #{col.type_spec}."}
426
+ END
427
+ })
428
+
429
+ # Column constraints
430
+ col.check_constraints.each do |cstrt|
431
+ tests << (dedent %Q{
432
+ IF NOT EXISTS (
433
+ SELECT * FROM sys.check_constraints cc
434
+ JOIN sys.columns c
435
+ ON c.object_id = cc.parent_object_id
436
+ AND c.column_id = cc.parent_column_id
437
+ JOIN sys.table_types tt ON tt.type_table_object_id = c.object_id
438
+ JOIN sys.schemas s ON s.schema_id = tt.schema_id
439
+ WHERE QUOTENAME(s.name) = #{t.schema.sql_quoted}
440
+ AND QUOTENAME(tt.name) = #{t.name.sql_quoted}
441
+ AND QUOTENAME(c.name) = #{col.name.sql_quoted}
442
+ AND cc.definition = #{cstrt.expression.sql_quoted}
443
+ )
444
+ BEGIN
445
+ #{adoption_error_sql "Expected CHECK constraint on #{col.name} of #{t.qualified_name} for #{cstrt.expression} not present."}
446
+ END
447
+ })
448
+ end
449
+ end
450
+
451
+ # Table constraints
452
+ t.constraints.select {|c| ['PRIMARY KEY', 'UNIQUE'].include? c.type}.each do |c|
453
+ tests << TableTypeKeyConstraintChecks.new(t, c, method(:adoption_error_sql)).to_s
454
+ end
455
+ t.constraints.select {|c| c.type == 'CHECK'}.each do |c|
456
+ tests << (dedent %Q{
457
+ IF NOT EXISTS (
458
+ SELECT * FROM sys.check_constraints cc
459
+ JOIN sys.table_types tt ON tt.type_table_object_id = cc.parent_object_id
460
+ JOIN sys.schemas s ON s.schema_id = tt.schema_id
461
+ WHERE QUOTENAME(s.name) = #{t.schema.sql_quoted}
462
+ AND QUOTENAME(tt.name) = #{t.name.sql_quoted}
463
+ AND cc.parent_column_id = 0
464
+ AND cc.definition = #{c.expression.sql_quoted}
465
+ )
466
+ BEGIN
467
+ #{adoption_error_sql "Expected CHECK constraint for #{c.expression} on #{t.qualified_name} not present."}
468
+ END
469
+ })
470
+ end
471
+ end.join("\n")
472
+ end
473
+
474
+ def check_clr_types
475
+ db_expectations.clr_types.map do |t|
476
+ dedent %Q{
477
+ IF NOT EXISTS (
478
+ SELECT * FROM sys.assembly_types t
479
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
480
+ WHERE QUOTENAME(s.name) = #{t.schema.sql_quoted} AND QUOTENAME(t.name) = #{t.name.sql_quoted}
481
+ )
482
+ BEGIN
483
+ #{adoption_error_sql "CLR type #{t.qualified_name} does not exist."}
484
+ END
485
+
486
+ IF NOT EXISTS (
487
+ SELECT * FROM sys.assembly_types t
488
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
489
+ JOIN sys.assemblies asm ON t.assembly_id = asm.assembly_id
490
+ WHERE QUOTENAME(s.name) = #{t.schema.sql_quoted} AND QUOTENAME(t.name) = #{t.name.sql_quoted}
491
+ AND QUOTENAME(asm.name) = #{t.assembly.sql_quoted}
492
+ )
493
+ BEGIN
494
+ #{adoption_error_sql "CLR type #{t.qualified_name} does not reference assembly #{t.assembly}."}
495
+ END
496
+
497
+ IF NOT EXISTS (
498
+ SELECT * FROM sys.assembly_types t
499
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
500
+ WHERE QUOTENAME(s.name) = #{t.schema.sql_quoted} AND QUOTENAME(t.name) = #{t.name.sql_quoted}
501
+ AND QUOTENAME(t.assembly_class) = #{t.clr_class.sql_quoted}
502
+ )
503
+ BEGIN
504
+ #{adoption_error_sql "CLR type #{t.qualified_name} does not reference class #{t.clr_class} of #{t.assembly}."}
505
+ END
506
+ }
507
+ end
508
+ end
509
+
510
+ def check_clr_aggregates
511
+ db_expectations.aggregates.map do |agg|
512
+ dedent %Q{
513
+ IF NOT EXISTS (
514
+ SELECT *
515
+ FROM sys.objects fn
516
+ JOIN sys.schemas s ON fn.schema_id = s.schema_id
517
+ WHERE fn.type = 'AF'
518
+ AND QUOTENAME(s.name) = #{agg.schema.sql_quoted}
519
+ AND QUOTENAME(fn.name) = #{agg.name.sql_quoted}
520
+ )
521
+ BEGIN
522
+ #{adoption_error_sql "CLR aggregate #{agg.qualified_name} does not exist."}
523
+ END
524
+
525
+ IF NOT EXISTS (
526
+ SELECT *
527
+ FROM sys.objects fn
528
+ JOIN sys.schemas s ON fn.schema_id = s.schema_id
529
+ JOIN sys.assembly_modules asmmod ON fn.object_id = asmmod.object_id
530
+ JOIN sys.assemblies asm ON asmmod.assembly_id = asm.assembly_id
531
+ WHERE fn.type = 'AF'
532
+ AND QUOTENAME(s.name) = #{agg.schema.sql_quoted}
533
+ AND QUOTENAME(fn.name) = #{agg.name.sql_quoted}
534
+ AND QUOTENAME(asm.name) = #{agg.clr_impl.assembly.sql_quoted}
535
+ )
536
+ BEGIN
537
+ #{adoption_error_sql "CLR aggregate #{agg.qualified_name} does not reference assembly #{agg.clr_impl.assembly}."}
538
+ END
539
+
540
+ IF NOT EXISTS (
541
+ SELECT *
542
+ FROM sys.objects fn
543
+ JOIN sys.schemas s ON fn.schema_id = s.schema_id
544
+ JOIN sys.assembly_modules asmmod ON fn.object_id = asmmod.object_id
545
+ JOIN sys.assemblies asm ON asmmod.assembly_id = asm.assembly_id
546
+ WHERE fn.type = 'AF'
547
+ AND QUOTENAME(s.name) = #{agg.schema.sql_quoted}
548
+ AND QUOTENAME(fn.name) = #{agg.name.sql_quoted}
549
+ AND QUOTENAME(asm.name) = #{agg.clr_impl.assembly.sql_quoted}
550
+ AND QUOTENAME(asmmod.assembly_class) = #{agg.clr_impl.asm_class.sql_quoted}
551
+ )
552
+ BEGIN
553
+ #{adoption_error_sql "CLR aggregate #{agg.qualified_name} does not reference class #{agg.clr_impl.asm_class} of #{agg.clr_impl.assembly}."}
554
+ END
555
+
556
+ IF NOT EXISTS (
557
+ SELECT *
558
+ FROM sys.objects fn
559
+ JOIN sys.schemas s ON fn.schema_id = s.schema_id
560
+ JOIN sys.assembly_modules asmmod ON fn.object_id = asmmod.object_id
561
+ WHERE fn.type = 'AF'
562
+ AND QUOTENAME(s.name) = #{agg.schema.sql_quoted}
563
+ AND QUOTENAME(fn.name) = #{agg.name.sql_quoted}
564
+ AND asmmod.execute_as_principal_id #{
565
+ case agg.execute_as
566
+ when nil
567
+ 'IS NULL'
568
+ when 'OWNER'
569
+ "= -2"
570
+ else
571
+ "= DATABASE_PRINCIPAL_ID(#{agg.execute_as.sql_quoted})"
572
+ end
573
+ }
574
+ )
575
+ BEGIN
576
+ #{adoption_error_sql "CLR aggregate #{agg.qualified_name} does not execute as #{
577
+ case agg.execute_as
578
+ when nil
579
+ 'CALLER'
580
+ else
581
+ agg.execute_as
582
+ end
583
+ }."}
584
+ END
204
585
  }
205
586
  end
206
587
  end
@@ -230,7 +611,7 @@ module Mkxms::Mssql
230
611
  def add_table_tests
231
612
  dsl {
232
613
  puts "IF NOT EXISTS (%s)" do
233
- puts %Q{
614
+ puts dedent %Q{
234
615
  SELECT * FROM sys.tables t
235
616
  INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
236
617
  WHERE t.name = #{table_name_literal}
@@ -261,30 +642,30 @@ module Mkxms::Mssql
261
642
  end
262
643
  )
263
644
  }
264
- puts "END;"
645
+ puts "END"
265
646
  puts
266
- QueryCursor.new(
267
- dedent(%Q{
268
- SELECT c.object_id, c.column_id
269
- FROM sys.columns c
270
- INNER JOIN sys.tables t ON c.object_id = t.object_id
271
- INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
272
- WHERE t.name = #{table_name_literal}
273
- AND s.name = #{schema_name_literal}
274
- ORDER BY c.column_id;
275
- }),
276
- "@column_object INT, @column_id INT",
277
- output_to: self
278
- ).expectations(
279
- on_extra: ->{puts error_sql "Table #{table_id} has one or more unexpected columns."},
280
- ) do |test|
281
- table.columns.each do |column|
282
- test.row(
283
- on_missing: ->{puts error_sql "Column #{column.name} not found where expected in #{table_id}."},
284
- ) {add_column_tests(column)}
285
- end
286
- end
287
647
  }
648
+ QueryCursor.new(
649
+ dedent(%Q{
650
+ SELECT c.object_id, c.column_id
651
+ FROM sys.columns c
652
+ INNER JOIN sys.tables t ON c.object_id = t.object_id
653
+ INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
654
+ WHERE t.name = #{table_name_literal}
655
+ AND s.name = #{schema_name_literal}
656
+ ORDER BY c.column_id;
657
+ }),
658
+ "@column_object INT, @column_id INT",
659
+ output_to: self
660
+ ).expectations(
661
+ on_extra: ->{puts error_sql "Table #{table_id} has one or more unexpected columns."},
662
+ ) do |test|
663
+ table.columns.each do |column|
664
+ test.row(
665
+ on_missing: ->{puts error_sql "Column #{column.name} not found where expected in #{table_id}."},
666
+ ) {add_column_tests(column)}
667
+ end
668
+ end
288
669
  end
289
670
 
290
671
  def add_column_tests(column)
@@ -317,11 +698,11 @@ module Mkxms::Mssql
317
698
  indented {
318
699
  puts error_sql "Column #{column.name} not found in expected position in #{table_id}."
319
700
  }
320
- puts "END;"
701
+ puts "END"
321
702
  }
322
- puts "END;"
703
+ puts "END"
323
704
  puts "IF @column_id IS NOT NULL"
324
- puts "BEGIN".."END;" do
705
+ puts "BEGIN".."END" do
325
706
  add_column_properties_test(column)
326
707
  end
327
708
  }
@@ -337,13 +718,13 @@ module Mkxms::Mssql
337
718
  conditions << %Q{c.is_computed = 1}
338
719
  conditions << compose_sql {
339
720
  puts "EXISTS (SELECT * FROM sys.computed_columns cc WHERE %s)" do
340
- puts "AND cc.object_id = c.object_id"
721
+ puts "cc.object_id = c.object_id"
341
722
  puts "AND cc.column_id = c.column_id"
342
723
  puts "AND cc.definition = %s" do
343
- strlit(column.computed_expression)
724
+ puts strlit(column.computed_expression)
344
725
  end
345
726
  puts "AND %s" do
346
- bit_test "cc.is_persisted", column.persisted?
727
+ puts(bit_test "cc.is_persisted", column.persisted?)
347
728
  end
348
729
  end
349
730
  }
@@ -396,7 +777,7 @@ module Mkxms::Mssql
396
777
  }
397
778
  conditions.each {|c| puts "AND " + c, :sub => nil}
398
779
  end
399
- puts "BEGIN".."END;" do
780
+ puts "BEGIN".."END" do
400
781
  puts error_sql "Column #{column.name} of #{table_id} #{mismatch_message}"
401
782
  end
402
783
  }
@@ -444,11 +825,11 @@ module Mkxms::Mssql
444
825
  puts strlit(unquoted_identifier col_dflt.name)
445
826
  end if col_dflt.name
446
827
  end
447
- puts("BEGIN".."END;") {
828
+ puts("BEGIN".."END") {
448
829
  puts adoption_error_sql("Column default constraint #{constraint_id} does not have the expected definition.")
449
830
  }
450
831
  }
451
- puts "END;"
832
+ puts "END"
452
833
  }
453
834
  end.join("\n")
454
835
  end
@@ -533,7 +914,7 @@ module Mkxms::Mssql
533
914
 
534
915
  check_column_sequence_end
535
916
  }
536
- puts "END;"
917
+ puts "END"
537
918
  }
538
919
  end
539
920
 
@@ -582,17 +963,17 @@ module Mkxms::Mssql
582
963
  IF @constraint_match_error = 0
583
964
  BEGIN
584
965
  SET @constraint_found = 1;
585
- END;
966
+ END
586
967
  }
587
968
  }
588
- puts "END;"
969
+ puts "END"
589
970
  puts dedent %Q{
590
971
  CLOSE constraint_cursor;
591
972
  DEALLOCATE constraint_cursor;
592
973
 
593
974
  IF @constraint_found = 0
594
975
  }
595
- puts "BEGIN".."END;" do
976
+ puts "BEGIN".."END" do
596
977
  puts error_sql "Expected #{cnstr_id} does not exist."
597
978
  end
598
979
  }
@@ -620,7 +1001,7 @@ module Mkxms::Mssql
620
1001
  FETCH NEXT FROM column_cursor INTO @column_name, @column_sorted_descending;
621
1002
  IF @@FETCH_STATUS = 0
622
1003
  }
623
- puts "BEGIN".."END;" do
1004
+ puts "BEGIN".."END" do
624
1005
  puts error_sql "#{cnstr_id.capitalize} has one or more unexpected columns."
625
1006
  end
626
1007
  puts "CLOSE column_cursor;"
@@ -654,7 +1035,7 @@ module Mkxms::Mssql
654
1035
  indented {
655
1036
  yield "Column #{index_column.name} should be sorted #{index_column.direction} in #{cnstr_id}."
656
1037
  }
657
- puts "END;"
1038
+ puts "END"
658
1039
  }
659
1040
  end
660
1041
  end
@@ -904,7 +1285,7 @@ module Mkxms::Mssql
904
1285
  indented {
905
1286
  puts error_sql "#{cnstr_id.capitalize} does not have expected definition."
906
1287
  }
907
- puts "END;"
1288
+ puts "END"
908
1289
  }
909
1290
  end
910
1291
 
@@ -921,7 +1302,7 @@ module Mkxms::Mssql
921
1302
  AND cc.definition = #{strlit cnstr.definition}
922
1303
  }
923
1304
  end
924
- puts "BEGIN".."END;" do
1305
+ puts "BEGIN".."END" do
925
1306
  puts error_sql "Expected #{cnstr_id} does not exist."
926
1307
  end
927
1308
  }
@@ -934,6 +1315,251 @@ module Mkxms::Mssql
934
1315
  end # Do not join -- each needs a separate batch (they use variables)
935
1316
  end
936
1317
 
1318
+ class DmlTriggerAdoptionChecks < IndentedStringBuilder
1319
+ include SqlStringManipulators
1320
+
1321
+ def initialize(trigger, tools)
1322
+ super()
1323
+
1324
+ @tools = tools
1325
+ @trigger = trigger
1326
+
1327
+ add_trigger_tests
1328
+ if trigger.clr_impl
1329
+ add_clr_impl_test
1330
+ else
1331
+ add_definition_test
1332
+ end
1333
+ end
1334
+
1335
+ attr_reader :trigger
1336
+
1337
+ def error_sql(s)
1338
+ @tools.adoption_error_sql(s)
1339
+ end
1340
+
1341
+ def check_definition_is(*args)
1342
+ @tools.definition_matches_by_hash(*args)
1343
+ end
1344
+
1345
+ def add_trigger_tests
1346
+ dsl {
1347
+ puts "IF NOT EXISTS (%s)" do
1348
+ puts dedent %Q{
1349
+ SELECT * FROM sys.triggers tgr
1350
+ JOIN sys.objects o ON tgr.object_id = o.object_id
1351
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
1352
+ WHERE QUOTENAME(s.name) = #{trigger.schema.sql_quoted}
1353
+ AND QUOTENAME(tgr.name) = #{trigger.name.sql_quoted}
1354
+ }
1355
+ end
1356
+ puts "BEGIN".."END" do
1357
+ puts error_sql "Expected trigger #{trigger.qualified_name} does not exist."
1358
+ end
1359
+
1360
+ puts "IF NOT EXISTS (%s)" do
1361
+ puts dedent %Q{
1362
+ SELECT * FROM sys.triggers tgr
1363
+ JOIN sys.objects o ON tgr.object_id = o.object_id
1364
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
1365
+ JOIN sys.tables t ON tgr.parent_id = t.object_id
1366
+ JOIN sys.schemas ts ON t.schema_id = ts.schema_id
1367
+ WHERE QUOTENAME(s.name) = #{trigger.schema.sql_quoted}
1368
+ AND QUOTENAME(tgr.name) = #{trigger.name.sql_quoted}
1369
+ AND QUOTENAME(ts.name) = #{trigger.table.schema.sql_quoted}
1370
+ AND QUOTENAME(t.name) = #{trigger.table.name.sql_quoted}
1371
+ }
1372
+ end
1373
+ puts "BEGIN".."END" do
1374
+ puts error_sql "Trigger #{trigger.qualified_name} does not apply to table #{trigger.table.qualified_name}."
1375
+ end
1376
+
1377
+ puts "IF NOT EXISTS (%s)" do
1378
+ execution_identity_test = (if trigger.execute_as == 'OWNER'
1379
+ "COALESCE(sql.execute_as_principal_id, asmmod.execute_as_principal_id) = -2"
1380
+ elsif trigger.execute_as
1381
+ "(QUOTENAME(p.name) = #{trigger.execute_as.sql_quoted} OR QUOTENAME(p2.name) = #{trigger.execute_as.sql_quoted})"
1382
+ else
1383
+ "COALESCE(sql.execute_as_principal_id, asmmod.execute_as_principal_id) IS NULL"
1384
+ end)
1385
+
1386
+ puts dedent %Q{
1387
+ SELECT * FROM sys.triggers tgr
1388
+ JOIN sys.objects o ON tgr.object_id = o.object_id
1389
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
1390
+ LEFT JOIN sys.sql_modules sql ON tgr.object_id = sql.object_id
1391
+ LEFT JOIN sys.database_principals p ON p.principal_id = sql.execute_as_principal_id
1392
+ LEFT JOIN sys.assembly_modules asmmod ON tgr.object_id = asmmod.object_id
1393
+ LEFT JOIN sys.database_principals pa ON pa.principal_id = asmmod.execute_as_principal_id
1394
+ WHERE QUOTENAME(s.name) = #{trigger.schema.sql_quoted}
1395
+ AND QUOTENAME(tgr.name) = #{trigger.name.sql_quoted}
1396
+ AND #{execution_identity_test}
1397
+ }
1398
+ end
1399
+ puts "BEGIN".."END" do
1400
+ if trigger.execute_as == 'OWNER'
1401
+ puts error_sql "Trigger #{trigger.qualified_name} does not execute as its owner."
1402
+ elsif trigger.execute_as
1403
+ puts error_sql "Trigger #{trigger.qualified_name} does not execute as #{trigger.execute_as}."
1404
+ else
1405
+ puts error_sql "Trigger #{trigger.qualified_name} does not execute as caller."
1406
+ end
1407
+ end
1408
+
1409
+ puts "IF NOT EXISTS (%s)" do
1410
+ is_instead_of_trigger_value = (trigger.timing.downcase == "after") ? 0 : 1
1411
+
1412
+ puts dedent %Q{
1413
+ SELECT * FROM sys.triggers tgr
1414
+ JOIN sys.objects o ON tgr.object_id = o.object_id
1415
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
1416
+ WHERE QUOTENAME(s.name) = #{trigger.schema.sql_quoted}
1417
+ AND QUOTENAME(tgr.name) = #{trigger.name.sql_quoted}
1418
+ AND tgr.is_instead_of_trigger = #{is_instead_of_trigger_value}
1419
+ }
1420
+ end
1421
+ puts "BEGIN".."END" do
1422
+ puts error_sql %Q{Trigger #{trigger.qualified_name} must occur #{trigger.timing} the handled event(s).}
1423
+ end
1424
+
1425
+ puts "IF NOT EXISTS (%s)" do
1426
+ puts dedent %Q{
1427
+ SELECT * FROM sys.triggers tgr
1428
+ JOIN sys.objects o ON tgr.object_id = o.object_id
1429
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
1430
+ WHERE QUOTENAME(s.name) = #{trigger.schema.sql_quoted}
1431
+ AND QUOTENAME(tgr.name) = #{trigger.name.sql_quoted}
1432
+ }
1433
+ trigger.events.each do |ev|
1434
+ puts dedent %Q{
1435
+ AND EXISTS (
1436
+ SELECT *
1437
+ FROM sys.events ev
1438
+ WHERE ev.object_id = tgr.object_id
1439
+ AND ev.type_desc = #{ev.sql_quoted}
1440
+ )
1441
+ }
1442
+ end
1443
+ end
1444
+ puts "BEGIN".."END" do
1445
+ puts error_sql %Q{Trigger #{trigger.qualified_name} must occur #{trigger.timing.downcase} #{trigger.events.join(' and ')}.}
1446
+ end
1447
+
1448
+ puts "IF NOT EXISTS (%s)" do
1449
+ is_not_for_replication_value = trigger.not_replicable ? 1 : 0
1450
+
1451
+ puts dedent %Q{
1452
+ SELECT * FROM sys.triggers tgr
1453
+ JOIN sys.objects o ON tgr.object_id = o.object_id
1454
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
1455
+ WHERE QUOTENAME(s.name) = #{trigger.schema.sql_quoted}
1456
+ AND QUOTENAME(tgr.name) = #{trigger.name.sql_quoted}
1457
+ AND tgr.is_not_for_replication = #{is_not_for_replication_value}
1458
+ }
1459
+ end
1460
+ puts "BEGIN".."END" do
1461
+ puts error_sql %Q{Trigger #{trigger.qualified_name} must#{' not' unless trigger.not_replicable} be configured "NOT FOR REPLICATION".}
1462
+ end
1463
+
1464
+ puts "IF NOT EXISTS (%s)" do
1465
+ is_disabled_value = trigger.disabled ? 1 : 0
1466
+
1467
+ puts dedent %Q{
1468
+ SELECT * FROM sys.triggers tgr
1469
+ JOIN sys.objects o ON tgr.object_id = o.object_id
1470
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
1471
+ WHERE QUOTENAME(s.name) = #{trigger.schema.sql_quoted}
1472
+ AND QUOTENAME(tgr.name) = #{trigger.name.sql_quoted}
1473
+ AND tgr.is_disabled = #{is_disabled_value}
1474
+ }
1475
+ end
1476
+ puts "BEGIN".."END" do
1477
+ puts error_sql "Trigger #{trigger.qualified_name} must#{' not' unless trigger.disabled} be disabled."
1478
+ end
1479
+ }
1480
+ end
1481
+
1482
+ def add_clr_impl_test
1483
+ dsl {
1484
+ # Check CLR implementation
1485
+ puts "IF NOT EXISTS(%s)" do
1486
+ puts dedent %Q{
1487
+ SELECT * FROM sys.triggers tgr
1488
+ JOIN sys.objects o ON tgr.object_id = o.object_id
1489
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
1490
+ JOIN sys.assembly_modules asmmod ON tgr.object_id = asmmod.object_id
1491
+ JOIN sys.assemblies asm ON asmmod.assembly_id = asm.assembly_id
1492
+ WHERE QUOTENAME(s.name) = #{trigger.schema.sql_quoted}
1493
+ AND QUOTENAME(tgr.name) = #{trigger.name.sql_quoted}
1494
+ AND QUOTENAME(asm.name) = #{trigger.clr_impl.assembly.sql_quoted}
1495
+ AND QUOTENAME(asmmod.assembly_class) = #{trigger.clr_impl.asm_class.sql_quoted}
1496
+ AND QUOTENAME(asmmod.assembly_method) = #{trigger.clr_impl.method.sql_quoted}
1497
+ }
1498
+ end
1499
+ puts "BEGIN".."END" do
1500
+ puts error_sql "Trigger #{trigger.qualified_name} does not invoke #{trigger.clr_impl.full_specifier}."
1501
+ end
1502
+ }
1503
+ end
1504
+
1505
+ def add_definition_test
1506
+ dsl {
1507
+ # Check definition
1508
+ puts "IF NOT EXISTS (%s)" do
1509
+ puts dedent %Q{
1510
+ SELECT * FROM sys.triggers tgr
1511
+ JOIN sys.objects o ON tgr.object_id = o.object_id
1512
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
1513
+ JOIN sys.sql_modules sql ON tgr.object_id = sql.object_id
1514
+ WHERE QUOTENAME(s.name) = #{trigger.schema.sql_quoted}
1515
+ AND QUOTENAME(tgr.name) = #{trigger.name.sql_quoted}
1516
+ AND #{check_definition_is("sql.definition", trigger.definition)}
1517
+ }
1518
+ end
1519
+ puts "BEGIN".."END" do
1520
+ puts error_sql "Trigger #{trigger.qualified_name} does not have the expected definition."
1521
+ end
1522
+ }
1523
+ end
1524
+ end
1525
+
1526
+ def check_dml_triggers
1527
+ db_expectations.dml_triggers.map do |tgr|
1528
+ DmlTriggerAdoptionChecks.new(tgr, self).to_s
1529
+ end
1530
+ end
1531
+
1532
+ def check_synonyms
1533
+ db_expectations.synonyms.map do |syn|
1534
+ dedent %Q{
1535
+ IF NOT EXISTS (
1536
+ SELECT * FROM sys.synonyms syn
1537
+ JOIN sys.schemas s ON syn.schema_id = s.schema_id
1538
+ WHERE QUOTENAME(s.name) = #{syn.schema.sql_quoted}
1539
+ AND QUOTENAME(syn.name) = #{syn.name.sql_quoted}
1540
+ )
1541
+ BEGIN
1542
+ #{adoption_error_sql "Synonym #{syn.qualified_name} does not exist."}
1543
+ END
1544
+
1545
+ IF NOT EXISTS (
1546
+ SELECT * FROM sys.synonyms syn
1547
+ JOIN sys.schemas s ON syn.schema_id = s.schema_id
1548
+ WHERE QUOTENAME(s.name) = #{syn.schema.sql_quoted}
1549
+ AND QUOTENAME(syn.name) = #{syn.name.sql_quoted}
1550
+ AND (
1551
+ OBJECT_ID(syn.base_object_name) IS NULL
1552
+ OR
1553
+ OBJECT_ID(syn.base_object_name) = OBJECT_ID(#{syn.referent.sql_quoted})
1554
+ )
1555
+ )
1556
+ BEGIN
1557
+ #{adoption_error_sql "Synonym #{syn.qualified_name} does not reference #{syn.referent}."}
1558
+ END
1559
+ }
1560
+ end
1561
+ end
1562
+
937
1563
  class IndexAdoptionChecks < IndentedStringBuilder
938
1564
  include SqlStringManipulators
939
1565
 
@@ -983,7 +1609,7 @@ module Mkxms::Mssql
983
1609
  # Key columns
984
1610
  QueryCursor.new(
985
1611
  dedent(%Q{
986
- SELECT c.column_name, ic.is_descending_key
1612
+ SELECT c.name, ic.is_descending_key
987
1613
  FROM sys.index_columns ic
988
1614
  JOIN sys.columns c
989
1615
  ON ic.object_id = c.object_id
@@ -1007,29 +1633,31 @@ module Mkxms::Mssql
1007
1633
  indented {
1008
1634
  puts error_sql "Expected #{column.name} as column #{i + 1} in #{index_id}."
1009
1635
  }
1010
- puts "END ELSE IF #{bit_test('@is_sorted_descending', column.direction == :descending)}"
1636
+ puts "END ELSE IF #{bit_test('@is_sorted_descending', column.direction != :descending)}"
1637
+ puts "BEGIN"
1011
1638
  indented {
1012
1639
  puts error_sql "Expected #{column.name} to be sorted #{column.direction} in #{index_id}."
1013
1640
  }
1014
- puts "END;"
1641
+ puts "END"
1015
1642
  }
1016
1643
  end
1017
1644
  end
1018
1645
 
1019
1646
  # Included columns
1020
- included_column_names = index.included_columns.map {|c| c.name}
1021
- puts "IF (%s) < #{included_column_names.length}" do
1022
- puts dedent %Q{
1023
- SELECT COUNT(*) FROM sys.index_columns ic
1024
- JOIN sys.columns c ON ic.object_id = c.object_id AND ic.index_id = c.index_id
1025
- WHERE ic.object_id = @relation_id
1026
- AND ic.index_id = @index_id
1027
- AND ic.key_ordinal = 0
1028
- AND QUOTENAME(c.name) IN (#{included_column_names.map {|s| strlit s}.join(', ')})
1029
- }
1030
- end
1031
- puts "BEGIN".."END" do
1032
- puts error_sql "#{index_id.capitalize} is missing one or more expected included columns."
1647
+ unless (included_column_names = index.included_columns.map {|c| c.name}).empty?
1648
+ puts "IF (%s) < #{included_column_names.length}" do
1649
+ puts dedent %Q{
1650
+ SELECT COUNT(*) FROM sys.index_columns ic
1651
+ JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
1652
+ WHERE ic.object_id = @relation_id
1653
+ AND ic.index_id = @index_id
1654
+ AND ic.key_ordinal = 0
1655
+ AND QUOTENAME(c.name) IN (#{included_column_names.map {|s| strlit s}.join(', ')})
1656
+ }
1657
+ end
1658
+ puts "BEGIN".."END" do
1659
+ puts error_sql "#{index_id.capitalize} is missing one or more expected included columns."
1660
+ end
1033
1661
  end
1034
1662
  }
1035
1663
 
@@ -1038,7 +1666,7 @@ module Mkxms::Mssql
1038
1666
 
1039
1667
  def index_property_check(expectation, expectation_desc)
1040
1668
  %Q{
1041
- IF NOT EXIST (
1669
+ IF NOT EXISTS (
1042
1670
  SELECT * FROM sys.indexes i
1043
1671
  WHERE i.object_id = @relation_id
1044
1672
  AND i.index_id = @index_id
@@ -1046,7 +1674,7 @@ module Mkxms::Mssql
1046
1674
  )
1047
1675
  BEGIN
1048
1676
  #{error_sql "#{@index_id.capitalize} should #{expectation_desc}."}
1049
- END;
1677
+ END
1050
1678
  }.strip.gsub(/\s+/, ' ')
1051
1679
  end
1052
1680
 
@@ -1128,7 +1756,7 @@ module Mkxms::Mssql
1128
1756
  end
1129
1757
  end
1130
1758
  }
1131
- puts "END;"
1759
+ puts "END"
1132
1760
  }
1133
1761
  end
1134
1762
 
@@ -1153,8 +1781,23 @@ module Mkxms::Mssql
1153
1781
  "INSERT INTO [xmigra].[access_objects] ([type], [name]) VALUES (N'#{type}', #{strlit qualified_name});"
1154
1782
  end
1155
1783
 
1156
- def definition_matches_by_hash(expr, definition)
1157
- "HASHBYTES('md5', #{expr}) = 0x#{Digest::MD5.hexdigest definition.gsub("\n", "\r\n").encode('UTF-16LE')}"
1784
+ # *indent* gives indentation for 2nd and later lines, or may be +false+ to
1785
+ # put all hash comparisons on the same line
1786
+ #
1787
+ # *** NEW IMPLEMENTATION IGNORES WHITESPACE WHEN COMPARING FUNCTION DEFINITIONS ***
1788
+ def definition_matches_by_hash(expr, definition, indent: ' ')
1789
+ digest_alg = Digest::MD5
1790
+ defn_reduced = definition.gsub(/\r|\n| /, "").encode(Encoding::UTF_16LE)
1791
+ (0..defn_reduced.length).step(4000).map do |start|
1792
+ begin
1793
+ digest_alg.hexdigest(defn_reduced[start, 4000])
1794
+ rescue Exception
1795
+ require 'pry'; binding.pry unless $STOP_PRYING || ($STOP_PRYING_FOR || {})[:context]
1796
+ raise
1797
+ end
1798
+ hash = digest_alg.hexdigest(defn_reduced[start, 4000])
1799
+ "HASHBYTES('md5', SUBSTRING(REPLACE(REPLACE(REPLACE(#{expr}, NCHAR(10), ''), NCHAR(13), ''), ' ', ''), #{start + 1}, 4000)) = 0x#{hash}"
1800
+ end.join("#{indent ? "\n" + indent : ' '}AND ")
1158
1801
  end
1159
1802
 
1160
1803
  def adopt_views_sql
@@ -1186,7 +1829,7 @@ module Mkxms::Mssql
1186
1829
  indented {
1187
1830
  puts adoption_error_sql "View #{view.qualified_name} does not have the expected definition."
1188
1831
  }
1189
- puts "END;"
1832
+ puts "END"
1190
1833
  puts access_object_adoption_sql(:VIEW, view.qualified_name)
1191
1834
  }
1192
1835
  end
@@ -1208,20 +1851,31 @@ module Mkxms::Mssql
1208
1851
  puts adoption_error_sql "Stored procedure #{sproc.qualified_name} does not exist."
1209
1852
  }
1210
1853
  puts "END ELSE IF NOT EXISTS (%s)" do
1211
- puts dedent %Q{
1212
- SELECT * FROM sys.procedures p
1213
- JOIN sys.schemas s ON p.schema_id = s.schema_id
1214
- JOIN sys.sql_modules sql ON p.object_id = sql.object_id
1215
- WHERE s.name = #{strlit(unquoted_identifier sproc.schema)}
1216
- AND p.name = #{strlit(unquoted_identifier sproc.name)}
1217
- AND #{definition_matches_by_hash('sql.definition', sproc.definition)}
1218
- }
1854
+ if sproc.clr_impl
1855
+ puts dedent %Q{
1856
+ SELECT * FROM sys.objects sproc
1857
+ JOIN sys.assembly_modules asmmod ON sproc.object_id = asmmod.object_id
1858
+ JOIN sys.assemblies asm ON asmmod.assembly_id = asm.assembly_id
1859
+ JOIN sys.schemas s ON sproc.schema_id = s.schema_id
1860
+ WHERE sproc.type = 'PC'
1861
+ AND QUOTENAME(asm.name) = #{sproc.clr_impl.assembly.sql_quoted}
1862
+ AND QUOTENAME(asmmod.assembly_class) = #{sproc.clr_impl.asm_class.sql_quoted}
1863
+ AND QUOTENAME(asmmod.assembly_method) = #{sproc.clr_impl.method.sql_quoted}
1864
+ }
1865
+ else
1866
+ puts dedent %Q{
1867
+ SELECT * FROM sys.procedures p
1868
+ JOIN sys.schemas s ON p.schema_id = s.schema_id
1869
+ JOIN sys.sql_modules sql ON p.object_id = sql.object_id
1870
+ WHERE s.name = #{strlit(unquoted_identifier sproc.schema)}
1871
+ AND p.name = #{strlit(unquoted_identifier sproc.name)}
1872
+ AND #{definition_matches_by_hash('sql.definition', sproc.definition)}
1873
+ }
1874
+ end
1219
1875
  end
1220
- puts "BEGIN"
1221
- indented {
1876
+ puts "BEGIN"..."END" do
1222
1877
  puts adoption_error_sql "Stored procedure #{sproc.qualified_name} does not have the expected definition."
1223
- }
1224
- puts "END;"
1878
+ end
1225
1879
  puts access_object_adoption_sql(:PROCEDURE, sproc.qualified_name)
1226
1880
  }
1227
1881
  end
@@ -1236,7 +1890,7 @@ module Mkxms::Mssql
1236
1890
  JOIN sys.schemas s ON fn.schema_id = s.schema_id
1237
1891
  WHERE s.name = #{strlit(unquoted_identifier udf.schema)}
1238
1892
  AND fn.name = #{strlit(unquoted_identifier udf.name)}
1239
- AND fn.type IN ('FN', 'IF', 'TF')
1893
+ AND fn.type IN ('FN', 'FS', 'FT', 'IF', 'TF')
1240
1894
  }
1241
1895
  end
1242
1896
  puts "BEGIN"
@@ -1244,20 +1898,34 @@ module Mkxms::Mssql
1244
1898
  puts adoption_error_sql "Function #{udf.qualified_name} does not exist."
1245
1899
  }
1246
1900
  puts "END ELSE IF NOT EXISTS (%s)" do
1247
- puts dedent %Q{
1248
- SELECT * FROM sys.objects fn
1249
- JOIN sys.schemas s ON fn.schema_id = s.schema_id
1250
- JOIN sys.sql_modules sql ON fn.object_id = sql.object_id
1251
- WHERE s.name = #{strlit(unquoted_identifier udf.schema)}
1252
- AND fn.name = #{strlit(unquoted_identifier udf.name)}
1253
- AND #{definition_matches_by_hash 'sql.definition', udf.definition}
1254
- }
1901
+ if udf.clr_impl
1902
+ puts dedent %Q{
1903
+ SELECT * FROM sys.objects fn
1904
+ JOIN sys.schemas s ON fn.schema_id = s.schema_id
1905
+ JOIN sys.assembly_modules asmmod ON fn.object_id = asmmod.object_id
1906
+ JOIN sys.assemblies asm ON asmmod.assembly_id = asm.assembly_id
1907
+ WHERE QUOTENAME(s.name) = #{udf.schema.sql_quoted}
1908
+ AND QUOTENAME(fn.name) = #{udf.name.sql_quoted}
1909
+ AND QUOTENAME(asm.name) = #{udf.clr_impl.assembly.sql_quoted}
1910
+ AND QUOTENAME(asmmod.assembly_class) = #{udf.clr_impl.asm_class.sql_quoted}
1911
+ AND QUOTENAME(asmmod.assembly_method) = #{udf.clr_impl.method.sql_quoted}
1912
+ }
1913
+ else
1914
+ puts dedent %Q{
1915
+ SELECT * FROM sys.objects fn
1916
+ JOIN sys.schemas s ON fn.schema_id = s.schema_id
1917
+ JOIN sys.sql_modules sql ON fn.object_id = sql.object_id
1918
+ WHERE s.name = #{strlit(unquoted_identifier udf.schema)}
1919
+ AND fn.name = #{strlit(unquoted_identifier udf.name)}
1920
+ AND #{definition_matches_by_hash 'sql.definition', udf.definition}
1921
+ }
1922
+ end
1255
1923
  end
1256
1924
  puts "BEGIN"
1257
1925
  indented {
1258
1926
  puts adoption_error_sql "Function #{udf.qualified_name} does not have the expected definition."
1259
1927
  }
1260
- puts "END;"
1928
+ puts "END"
1261
1929
  puts access_object_adoption_sql(:FUNCTION, udf.qualified_name)
1262
1930
  }
1263
1931
  end