mkxms-mssql 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +53 -0
  6. data/Rakefile +1 -0
  7. data/bin/mkxms-mssql +5 -0
  8. data/lib/mkxms/mssql/access_object_definition.rb +61 -0
  9. data/lib/mkxms/mssql/adoption_script_writer.rb +1486 -0
  10. data/lib/mkxms/mssql/check_constraint_handler.rb +56 -0
  11. data/lib/mkxms/mssql/database_handler.rb +339 -0
  12. data/lib/mkxms/mssql/default_constraint_handler.rb +46 -0
  13. data/lib/mkxms/mssql/engine.rb +88 -0
  14. data/lib/mkxms/mssql/exceptions.rb +10 -0
  15. data/lib/mkxms/mssql/filegroup_handler.rb +81 -0
  16. data/lib/mkxms/mssql/foreign_key_handler.rb +85 -0
  17. data/lib/mkxms/mssql/function_handler.rb +74 -0
  18. data/lib/mkxms/mssql/indented_string_builder.rb +199 -0
  19. data/lib/mkxms/mssql/index_column.rb +11 -0
  20. data/lib/mkxms/mssql/index_handler.rb +98 -0
  21. data/lib/mkxms/mssql/keylike_constraint_helper.rb +67 -0
  22. data/lib/mkxms/mssql/permission_handler.rb +115 -0
  23. data/lib/mkxms/mssql/primary_key_handler.rb +36 -0
  24. data/lib/mkxms/mssql/property_handler.rb +87 -0
  25. data/lib/mkxms/mssql/query_cursor.rb +111 -0
  26. data/lib/mkxms/mssql/role_handler.rb +55 -0
  27. data/lib/mkxms/mssql/schema_handler.rb +42 -0
  28. data/lib/mkxms/mssql/sql_string_manipulators.rb +46 -0
  29. data/lib/mkxms/mssql/statistics_handler.rb +59 -0
  30. data/lib/mkxms/mssql/stored_procedure_handler.rb +65 -0
  31. data/lib/mkxms/mssql/table_handler.rb +180 -0
  32. data/lib/mkxms/mssql/unique_constraint_handler.rb +32 -0
  33. data/lib/mkxms/mssql/utils.rb +83 -0
  34. data/lib/mkxms/mssql/version.rb +5 -0
  35. data/lib/mkxms/mssql/view_handler.rb +58 -0
  36. data/lib/mkxms/mssql.rb +62 -0
  37. data/mkxms-mssql.gemspec +26 -0
  38. data/spec/utils/indented_string_builder_spec.rb +218 -0
  39. data/spec/utils/query_cursor_spec.rb +57 -0
  40. metadata +142 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b79eb5cd26ad73ff11edc3d488344e569a723439
4
+ data.tar.gz: d2dd1a1381d3d715b2dd5dfb066c7aed4f7f4b6e
5
+ SHA512:
6
+ metadata.gz: 9ad174edd9c865f2c5687567dd98a7a66bd38559ba0fc28715768d4dd03dc24d0830d8ff6baa5e214f6f7c97682bdd7e92eb9f206f7a14ecf1e1fb89b5ddc493
7
+ data.tar.gz: 1fa9637d1da5cc828ef1d67f886144a98f6143433e5802a36af73117d03ed1c9b3e30b06da95edd8b3db567340a7d810aebb9ad1bd0dfb082863874e4a4bd284
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mkxms-mssql.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Richard Weeks
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Mkxms::Mssql
2
+
3
+ Creates a set of XMigra (https://rubygems.org/gems/xmigra) source files
4
+ from a database description XML document (as generated by
5
+ [mssql-eyewkas.sql](https://gist.github.com/rtweeks/62d8fb9c6ca3de1195d9#file-mssql-eyewkas-sql)
6
+ or a compatible later version).
7
+
8
+ ## Installation
9
+
10
+ Install it as a gem:
11
+
12
+ $ gem install mkxms-mssql
13
+
14
+ ## Usage
15
+
16
+ Run `mkxms-mssql [-o DEST_DIR] [DB_DESCRIPTION_XML_FILE]`
17
+
18
+ ## Project Status
19
+
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
22
+ features are NOT supported by this program (although they may be supported
23
+ by XMigra):
24
+
25
+ * Partition functions and partition schemes
26
+ * CLR assemblies
27
+ * Synonyms
28
+ * XML schema collections
29
+ * User-defined (SQL) types - scalar or table
30
+ * CLR types
31
+ * Fulltext indexes
32
+ * Rules
33
+ * Triggers
34
+ * Spatial indexes
35
+ * CLR stored procedures
36
+ * CLR functions
37
+ * Service Broker configuration
38
+
39
+ Any elements in the database description XML relating to the features above
40
+ (or any other feature not supported by this tool) will result in an unknown
41
+ method error from this tool. This tool is definitely still under development!
42
+ If your database doesn't involve any of the features listed here (or you're
43
+ willing to work around those it does), great. If it does, please help make
44
+ this tool better -- contribute a pull request or at least open or up-vote an
45
+ issue for the feature.
46
+
47
+ ## Contributing
48
+
49
+ 1. Fork it ( http://github.com/rtweeks/mkxms-mssql/fork )
50
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
51
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
52
+ 4. Push to the branch (`git push origin my-new-feature`)
53
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/mkxms-mssql ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mkxms/mssql'
4
+
5
+ Mkxms::Mssql.run_program
@@ -0,0 +1,61 @@
1
+ require 'mkxms/mssql/utils'
2
+
3
+ module Mkxms; end
4
+
5
+ module Mkxms::Mssql
6
+ module AccessObjectDefinition
7
+ class Scanner
8
+ def initialize(dfn)
9
+ @dfn = dfn
10
+ @start = 0
11
+ end
12
+
13
+ attr_reader :last_match
14
+
15
+ def next_is(re)
16
+ if (m = re.match(@dfn, @start)) && (m.begin(0) <= @start)
17
+ @start = m.end(0)
18
+ return @last_match = m
19
+ end
20
+ end
21
+
22
+ def remaining?
23
+ @start < @dfn.length
24
+ end
25
+ end
26
+
27
+ def self.replace_object_name(dfn, s)
28
+ scan = Scanner.new(dfn)
29
+ looking_for = :create
30
+ name_start = name_end = nil
31
+ while scan.remaining?
32
+ case
33
+ when scan.next_is(/\s+/) # whitespace
34
+ when scan.next_is(/--.*?\n/) # one line comment
35
+ when scan.next_is(%r{/\*.*?\*/}m) # delimited comment
36
+ nil
37
+ when looking_for.equal?(:create) && scan.next_is(/CREATE\s/i)
38
+ looking_for = :object_type
39
+ when looking_for.equal?(:object_type) && scan.next_is(/(VIEW|PROC(EDURE)?|FUNCTION)\s/i)
40
+ looking_for = :object_name
41
+ when looking_for.equal?(:object_name) && scan.next_is(/[a-z][a-z0-9_]*|\[([^\]]|\]\])+\]/i)
42
+ name_start ||= scan.last_match.begin(0)
43
+ name_end = scan.last_match.end(0)
44
+ looking_for = :qualifier_mark
45
+ when looking_for.equal?(:qualifier_mark) && scan.next_is(/\./)
46
+ looking_for = :object_name
47
+ when looking_for.equal?(:qualifier_mark) && scan.next_is(/[^.]/)
48
+ break
49
+ end
50
+ end
51
+
52
+ dfn.dup.tap do |result|
53
+ result[name_start...name_end] = s
54
+
55
+ # These two steps keep the SQL from being in double-quoted scalar format:
56
+ result.gsub!(/\s+\n/, "\n")
57
+ result.replace(Utils.expand_tabs(result, tab_width: 4))
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,1486 @@
1
+ require 'pathname'
2
+ require 'xmigra'
3
+ require 'mkxms/mssql/indented_string_builder'
4
+ require 'mkxms/mssql/query_cursor'
5
+ require 'mkxms/mssql/sql_string_manipulators'
6
+
7
+ module Mkxms; end
8
+
9
+ module Mkxms::Mssql
10
+ class AdoptionScriptWriter
11
+ include XMigra::MSSQLSpecifics
12
+ include SqlStringManipulators
13
+
14
+ def initialize(db_expectations)
15
+ @db_expectations = db_expectations
16
+ # Ex nihilo DB schema builder
17
+ @xn_builder = XMigra::SchemaUpdater.new(@db_expectations.schema_dir)
18
+ end
19
+
20
+ attr_reader :db_expectations
21
+
22
+ def create_script(path)
23
+ Pathname(path).open('w') do |script|
24
+ script.puts adoption_sql
25
+ end
26
+ end
27
+
28
+ def adoption_sql
29
+ in_ddl_transaction do
30
+ script_parts = [
31
+ # Check for blatantly incorrect application of script, e.g. running
32
+ # on master or template database.
33
+ :check_execution_environment_sql,
34
+
35
+ # Create schema version control (SVC) tables if they don't exist
36
+ :ensure_version_tables_sql,
37
+ :ensure_permissions_table_sql,
38
+
39
+ # Create an error table
40
+ :create_adoption_error_table_sql,
41
+
42
+ # Check roles
43
+ :check_expected_roles_exist_sql,
44
+ :check_expected_role_membership_sql,
45
+
46
+ # Check schemas
47
+ :check_expected_schemas_exist_sql,
48
+
49
+ # Check tables (including columns)
50
+ :check_tables_exist_and_structured_as_expected_sql,
51
+
52
+ # Check column defaults
53
+ :check_expected_column_defaults_exist_sql,
54
+
55
+ # Check primary key and unique constraints
56
+ :check_primary_key_and_unique_constraints_sql,
57
+
58
+ # Check foreign key constraints
59
+ :check_foreign_key_constraints_sql,
60
+
61
+ # Check check constraints
62
+ :check_check_constraints_sql,
63
+
64
+ # Adopt indexes
65
+ :adopt_indexes_sql,
66
+
67
+ # Adopt statistics
68
+ :adopt_statistics_sql,
69
+
70
+ # Adopt views
71
+ :adopt_views_sql,
72
+
73
+ # Adopt stored procedures
74
+ :adopt_stored_procedures_sql,
75
+
76
+ # Adopt user defined functions
77
+ :adopt_udfs_sql,
78
+
79
+ # Adopt permissions
80
+ :adopt_permissions_sql,
81
+
82
+ # Error out if there are any entries in the error table
83
+ :check_adoption_error_table_empty_sql,
84
+
85
+ # Write version bridge record to xmigra.applied
86
+ :write_version_bridge_record_sql,
87
+ ]
88
+
89
+ #script_parts = script_parts.map {|mn| self.send(mn)}.flatten.compact
90
+ script_parts = script_parts.map do |mn|
91
+ [
92
+ %Q{PRINT N'ADOPTION STEP: #{mn}';},
93
+ self.send(mn)
94
+ ]
95
+ end.flatten.compact
96
+ script_parts.join(ddl_block_separator)
97
+ end
98
+ end
99
+
100
+ def compose_sql(&blk)
101
+ IndentedStringBuilder.dsl(&blk)
102
+ end
103
+
104
+ begin # Adoption error handling methods
105
+ def create_adoption_error_table_sql
106
+ dedent %Q{
107
+ IF EXISTS (
108
+ SELECT * FROM sys.objects o WHERE o.object_id = OBJECT_ID(N'[xmigra].[adoption_errors]')
109
+ )
110
+ BEGIN
111
+ DROP TABLE [xmigra].[adoption_errors];
112
+ END;
113
+ GO
114
+
115
+ CREATE TABLE [xmigra].[adoption_errors] (
116
+ [message] nvarchar(1000)
117
+ );
118
+ }
119
+ end
120
+
121
+ def adoption_error_sql(message)
122
+ "INSERT INTO [xmigra].[adoption_errors] (message) VALUES (#{strlit(message)});"
123
+ end
124
+
125
+ def check_adoption_error_table_empty_sql
126
+ dedent %Q{
127
+ IF EXISTS (
128
+ SELECT TOP 1 * FROM [xmigra].[adoption_errors]
129
+ )
130
+ BEGIN
131
+ SELECT * FROM [xmigra].[adoption_errors];
132
+ RAISERROR (N'Database adoption failed.', 11, 1);
133
+ END;
134
+
135
+ DROP TABLE [xmigra].[adoption_errors];
136
+ }
137
+ end
138
+ end
139
+
140
+ def check_expected_roles_exist_sql
141
+ db_expectations.roles.map do |r|
142
+ dedent %Q{
143
+ IF NOT EXISTS (
144
+ SELECT * FROM sys.database_principals r
145
+ WHERE r.name = #{strlit(unquoted_identifier r.name)}
146
+ AND r.type = 'R'
147
+ )
148
+ BEGIN
149
+ #{adoption_error_sql "Role #{r.name} does not exist."}
150
+ END;
151
+
152
+ IF EXISTS (
153
+ SELECT * FROM sys.database_principals r
154
+ INNER JOIN sys.database_principals o ON r.owning_principal_id = o.principal_id
155
+ WHERE r.name = #{strlit(unquoted_identifier r.name)}
156
+ AND o.name <> #{strlit(unquoted_identifier r.owner)}
157
+ )
158
+ BEGIN
159
+ #{adoption_error_sql "Role #{r.name} should be owned by #{r.owner}."}
160
+ END;
161
+ }
162
+ end.join("\n")
163
+ end
164
+
165
+ def check_expected_role_membership_sql
166
+ [].tap do |tests|
167
+ db_expectations.roles.each do |r|
168
+ r.encompassing_roles.each do |er_name|
169
+ tests << dedent(%Q{
170
+ IF NOT EXISTS (
171
+ SELECT * FROM sys.database_role_members rm
172
+ INNER JOIN sys.database_principals r ON rm.member_principal_id = r.principal_id
173
+ INNER JOIN sys.database_principals er ON rm.role_principal_id = er.principal_id
174
+ WHERE r.name = #{strlit(unquoted_identifier r.name)}
175
+ AND er.name = #{strlit(unquoted_identifier er_name)}
176
+ )
177
+ BEGIN
178
+ #{adoption_error_sql "Role #{r.name} should be a member of #{er_name}."}
179
+ END;
180
+ })
181
+ end
182
+ end
183
+ end.join("\n")
184
+ end
185
+
186
+ def check_expected_schemas_exist_sql
187
+ db_expectations.schemas.map do |schema|
188
+ dedent %Q{
189
+ IF NOT EXISTS (
190
+ SELECT * FROM sys.schemas s
191
+ WHERE s.name = #{strlit(unquoted_identifier schema.name)}
192
+ )
193
+ BEGIN
194
+ #{adoption_error_sql "Schema #{schema.name} does not exist."}
195
+ END ELSE IF NOT EXISTS (
196
+ SELECT * FROM sys.schemas s
197
+ INNER JOIN sys.database_principals r ON s.principal_id = r.principal_id
198
+ WHERE s.name = #{strlit(unquoted_identifier schema.name)}
199
+ AND r.name = #{strlit(unquoted_identifier schema.owner)}
200
+ )
201
+ BEGIN
202
+ #{adoption_error_sql "Schema #{schema.name} is not owned by #{schema.owner}."}
203
+ END;
204
+ }
205
+ end
206
+ end
207
+
208
+ class TableAdoptionChecks < IndentedStringBuilder
209
+ include SqlStringManipulators
210
+ extend SqlStringManipulators
211
+
212
+ def initialize(table, error_sql_proc)
213
+ super()
214
+
215
+ @table = table
216
+ @schema_name_literal = strlit(unquoted_identifier table.schema)
217
+ @table_name_literal = strlit(unquoted_identifier table.name)
218
+ @table_id = [table.schema, table.name].join('.')
219
+ @error_sql_proc = error_sql_proc
220
+
221
+ add_table_tests
222
+ end
223
+
224
+ attr_reader :table, :schema_name_literal, :table_name_literal, :table_id
225
+
226
+ def error_sql(s)
227
+ @error_sql_proc.call(s)
228
+ end
229
+
230
+ def add_table_tests
231
+ dsl {
232
+ puts "IF NOT EXISTS (%s)" do
233
+ puts %Q{
234
+ SELECT * FROM sys.tables t
235
+ INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
236
+ WHERE t.name = #{table_name_literal}
237
+ AND s.name = #{schema_name_literal}
238
+ }
239
+ end
240
+ puts "BEGIN"
241
+ indented {
242
+ puts error_sql "Table #{table_id} does not exist."
243
+ }
244
+ puts "END ELSE IF NOT EXISTS (%s)" do
245
+ puts dedent %Q{
246
+ SELECT * FROM sys.tables t
247
+ INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
248
+ LEFT JOIN sys.database_principals r ON t.principal_id = r.principal_id
249
+ WHERE t.name = #{table_name_literal}
250
+ AND s.name = #{schema_name_literal}
251
+ AND r.name #{table.owner ? "= " + strlit(unquoted_identifier(table.owner)) : "IS NULL"}
252
+ }
253
+ end
254
+ puts "BEGIN"
255
+ indented {
256
+ puts error_sql(
257
+ if table.owner
258
+ "Table #{table_id} is not owned (explicitly) by #{table.owner}."
259
+ else
260
+ "Table #{table_id} is specified as other than the schema owner."
261
+ end
262
+ )
263
+ }
264
+ puts "END;"
265
+ 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
+ }
288
+ end
289
+
290
+ def add_column_tests(column)
291
+ column_name_literal = strlit(unquoted_identifier column.name)
292
+
293
+ dsl {
294
+ puts "IF NOT EXISTS (%s)" do
295
+ puts dedent %Q{
296
+ SELECT * FROM sys.columns c
297
+ WHERE c.object_id = @column_object
298
+ AND c.column_id = @column_id
299
+ AND c.name = #{column_name_literal}
300
+ }
301
+ end
302
+ puts "BEGIN"
303
+ indented {
304
+ puts dedent %Q{
305
+ SET @column_id = (
306
+ SELECT c.column_id FROM sys.columns c
307
+ WHERE c.object_id = @column_object
308
+ AND c.name = #{column_name_literal}
309
+ );
310
+ }
311
+ puts "IF @column_id IS NULL"
312
+ puts "BEGIN"
313
+ indented {
314
+ puts error_sql "Column #{column.name} not found in #{table_id}."
315
+ }
316
+ puts "END ELSE BEGIN"
317
+ indented {
318
+ puts error_sql "Column #{column.name} not found in expected position in #{table_id}."
319
+ }
320
+ puts "END;"
321
+ }
322
+ puts "END;"
323
+ puts "IF @column_id IS NOT NULL"
324
+ puts "BEGIN".."END;" do
325
+ add_column_properties_test(column)
326
+ end
327
+ }
328
+ end
329
+
330
+ NON_ANSI_PADDABLE_TYPES = %w[char varchar binary varbinary]
331
+ UNICODE_CHAR_TYPES = %w[nchar nvarchar]
332
+ def add_column_properties_test(column)
333
+ conditions = []
334
+ if column.computed_expression
335
+ mismatch_message = "does not have the expected definition"
336
+
337
+ conditions << %Q{c.is_computed = 1}
338
+ conditions << compose_sql {
339
+ puts "EXISTS (SELECT * FROM sys.computed_columns cc WHERE %s)" do
340
+ puts "AND cc.object_id = c.object_id"
341
+ puts "AND cc.column_id = c.column_id"
342
+ puts "AND cc.definition = %s" do
343
+ strlit(column.computed_expression)
344
+ end
345
+ puts "AND %s" do
346
+ bit_test "cc.is_persisted", column.persisted?
347
+ end
348
+ end
349
+ }
350
+ else
351
+ type_str = [].tap {|parts| column.each_type_part {|part| parts << part}}.join(' ')
352
+ mismatch_message = "is not #{type_str}"
353
+
354
+ conditions << "ct.name = %s" % [strlit(unquoted_identifier column.type_info[:type])]
355
+ type_schema = column.type_info[:type_schema] || 'sys'
356
+ col_type_is_sys_type = unquoted_identifier(type_schema).downcase == 'sys'
357
+ comparable_col_type = unquoted_identifier(column.type_info[:type]).downcase
358
+ conditions << compose_sql {
359
+ puts "EXISTS (SELECT * FROM sys.schemas cts WHERE %s)" do
360
+ puts "cts.schema_id = ct.schema_id"
361
+ puts "AND cts.name = #{strlit(unquoted_identifier type_schema)}"
362
+ end
363
+ }
364
+ if precision = column.type_info[:precision]
365
+ conditions << %Q{c.precision = #{precision}}
366
+ end
367
+ if scale = column.type_info[:scale]
368
+ conditions << %Q{c.scale = #{scale}}
369
+ end
370
+ if capacity = column.type_info[:capacity]
371
+ conditions << (if capacity == 'max'
372
+ %Q{c.max_length = -1}
373
+ elsif col_type_is_sys_type && %w[nchar nvarchar].include?(comparable_col_type)
374
+ %Q{c.max_length = #{capacity.to_i * 2}}
375
+ else
376
+ %Q{c.max_length = #{capacity}}
377
+ end)
378
+ end
379
+ conditions << %Q{c.collation_name = #{strlit column.collation}} if column.collation
380
+ conditions << bit_test("c.is_identity", column.identity?)
381
+ conditions << bit_test("c.is_rowguidcol", column.rowguid?)
382
+ conditions << bit_test("c.is_filestream", column.filestream?)
383
+ conditions << bit_test("c.is_nullable", column.nullable?)
384
+ if col_type_is_sys_type && NON_ANSI_PADDABLE_TYPES.include?(comparable_col_type)
385
+ conditions << bit_test("c.is_ansi_padded", true)
386
+ end
387
+ end
388
+
389
+ dsl {
390
+ puts "IF NOT EXISTS (%s)" do
391
+ puts dedent %Q{
392
+ SELECT * FROM sys.columns c
393
+ INNER JOIN sys.types ct ON c.user_type_id = ct.user_type_id
394
+ WHERE c.object_id = @column_object
395
+ AND c.column_id = @column_id
396
+ }
397
+ conditions.each {|c| puts "AND " + c, :sub => nil}
398
+ end
399
+ puts "BEGIN".."END;" do
400
+ puts error_sql "Column #{column.name} of #{table_id} #{mismatch_message}"
401
+ end
402
+ }
403
+ end
404
+
405
+ def compose_sql(&blk)
406
+ IndentedStringBuilder.dsl(&blk)
407
+ end
408
+ end
409
+
410
+ def check_tables_exist_and_structured_as_expected_sql
411
+ db_expectations.tables.map do |table|
412
+ TableAdoptionChecks.new(table, method(:adoption_error_sql)).to_s
413
+ end # Do not join -- each needs a separate batch (they use variables)
414
+ end
415
+
416
+ def check_expected_column_defaults_exist_sql
417
+ db_expectations.column_defaults.map do |col_dflt|
418
+ constraint_id = (col_dflt.name || "on #{col_dflt.column}") + " of #{col_dflt.qualified_table}"
419
+ compose_sql {
420
+ puts "IF NOT EXISTS (%s)" do
421
+ puts "SELECT * FROM sys.default_constraints dc"
422
+ puts "INNER JOIN sys.schemas s ON dc.schema_id = s.schema_id"
423
+ puts "INNER JOIN sys.tables t ON dc.parent_object_id = t.object_id"
424
+ puts "INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id"
425
+ puts "WHERE dc.name = %s" do
426
+ puts strlit(unquoted_identifier col_dflt.name)
427
+ end if col_dflt.name
428
+ end
429
+ puts "BEGIN"
430
+ indented {puts adoption_error_sql(
431
+ "Expected column default constraint #{constraint_id} does not exist."
432
+ )}
433
+ puts "END ELSE BEGIN"
434
+ indented {
435
+ puts "IF NOT EXISTS (%s)" do
436
+ puts "SELECT * FROM sys.default_constraints dc"
437
+ puts "INNER JOIN sys.schemas s ON dc.schema_id = s.schema_id"
438
+ puts "INNER JOIN sys.tables t ON dc.parent_object_id = t.object_id"
439
+ puts "INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id"
440
+ puts "WHERE dc.definition = %s" do
441
+ puts strlit col_dflt.expression
442
+ end
443
+ puts "AND dc.name = %s" do
444
+ puts strlit(unquoted_identifier col_dflt.name)
445
+ end if col_dflt.name
446
+ end
447
+ puts("BEGIN".."END;") {
448
+ puts adoption_error_sql("Column default constraint #{constraint_id} does not have the expected definition.")
449
+ }
450
+ }
451
+ puts "END;"
452
+ }
453
+ end.join("\n")
454
+ end
455
+
456
+ class KeylikeConstraintAdoptionChecks < IndentedStringBuilder
457
+ include SqlStringManipulators
458
+
459
+ def initialize(cnstr, error_sql_proc)
460
+ super()
461
+
462
+ @cnstr = cnstr
463
+ @error_sql_proc = error_sql_proc
464
+ @constraint_type = cnstr.sql_constraint_type.downcase
465
+ @cnstr_id = (
466
+ "#{constraint_type} constraint%s on #{cnstr.qualified_table}" % [
467
+ cnstr.name ? " " + cnstr.name : ''
468
+ ]
469
+ )
470
+
471
+ if cnstr.name
472
+ add_named_constraint_tests
473
+ else
474
+ add_unnamed_constraint_tests
475
+ end
476
+ end
477
+
478
+ attr_reader :cnstr, :cnstr_id, :constraint_type
479
+
480
+ def error_sql(s)
481
+ @error_sql_proc.call(s)
482
+ end
483
+
484
+ def add_named_constraint_tests
485
+ dsl {
486
+ puts "IF NOT EXISTS (%s)" do
487
+ puts dedent %Q{
488
+ SELECT * FROM sys.key_constraints kc
489
+ INNER JOIN sys.schemas s ON kc.schema_id = s.schema_id
490
+ INNER JOIN sys.tables t ON kc.parent_object_id = t.object_id
491
+ INNER JOIN sys.indexes i ON kc.parent_object_id = i.object_id AND kc.unique_index_id = i.index_id
492
+ }
493
+ puts "WHERE s.name = %s" do
494
+ puts strlit(unquoted_identifier cnstr.schema)
495
+ end
496
+ puts "AND t.name = %s" do
497
+ puts strlit(unquoted_identifier cnstr.table)
498
+ end
499
+ puts "AND kc.name = %s" do
500
+ puts strlit(unquoted_identifier cnstr.name)
501
+ end
502
+ end
503
+ puts "BEGIN"
504
+ indented {
505
+ puts error_sql "#{cnstr_id.capitalize} does not exist."
506
+ }
507
+ puts "END ELSE BEGIN"
508
+ indented {
509
+ # Check that this constraint covers the correct fields, noting
510
+ # that the constraint doesn't exist if cnstr.name.nil? or that
511
+ # it doesn't have the expected fields, otherwise.
512
+ declare_column_sequence_cursor_with_conditions {
513
+ puts dedent %Q{
514
+ INNER JOIN sys.schemas s ON kc.schema_id = s.schema_id
515
+ INNER JOIN sys.tables t ON kc.parent_object_id = t.object_id
516
+ }
517
+ puts "WHERE s.name = %s" do
518
+ puts strlit(unquoted_identifier cnstr.schema)
519
+ end
520
+ puts "AND t.name = %s" do
521
+ puts strlit(unquoted_identifier cnstr.table)
522
+ end
523
+ puts "AND kc.name = %s" do
524
+ puts strlit(unquoted_identifier cnstr.name)
525
+ end
526
+ }
527
+
528
+ cnstr.columns.each do |index_column|
529
+ add_column_sequence_test(index_column) do |error_message|
530
+ puts error_sql error_message
531
+ end
532
+ end
533
+
534
+ check_column_sequence_end
535
+ }
536
+ puts "END;"
537
+ }
538
+ end
539
+
540
+ def add_unnamed_constraint_tests
541
+ dsl {
542
+ puts dedent %Q{
543
+ DECLARE @constraint_id INT;
544
+
545
+ DECLARE constraint_cursor CURSOR FOR
546
+ SELECT kc.object_id
547
+ FROM sys.key_constraints kc
548
+ INNER JOIN sys.schemas s ON kc.schema_id = s.schema_id
549
+ INNER JOIN sys.tables t ON kc.parent_object_id = t.object_id
550
+ }
551
+ puts "WHERE s.name = %s" do
552
+ puts strlit(unquoted_identifier cnstr.schema)
553
+ end
554
+ puts "AND t.name = %s" do
555
+ puts strlit(unquoted_identifier cnstr.table)
556
+ end
557
+ puts ";"
558
+ puts "OPEN constraint_cursor;"
559
+
560
+ puts dedent %Q{
561
+ DECLARE @constraint_found BIT, @constraint_match_error BIT;
562
+ SET @constraint_found = 0;
563
+ FETCH NEXT FROM constraint_cursor INTO @constraint_id;
564
+ WHILE @@FETCH_STATUS = 0 AND @constraint_found = 0
565
+ BEGIN
566
+ }
567
+ indented {
568
+ puts "SET @constraint_match_error = 0;"
569
+ declare_column_sequence_cursor_with_conditions {
570
+ puts "WHERE kc.object_id = @constraint_id"
571
+ }
572
+
573
+ cnstr.columns.each do |index_column|
574
+ add_column_sequence_test(index_column) do |error_message|
575
+ puts "SET @constraint_match_error = 1;"
576
+ end
577
+ end
578
+
579
+ check_column_sequence_end
580
+
581
+ puts %Q{
582
+ IF @constraint_match_error = 0
583
+ BEGIN
584
+ SET @constraint_found = 1;
585
+ END;
586
+ }
587
+ }
588
+ puts "END;"
589
+ puts dedent %Q{
590
+ CLOSE constraint_cursor;
591
+ DEALLOCATE constraint_cursor;
592
+
593
+ IF @constraint_found = 0
594
+ }
595
+ puts "BEGIN".."END;" do
596
+ puts error_sql "Expected #{cnstr_id} does not exist."
597
+ end
598
+ }
599
+ end
600
+
601
+ def declare_column_sequence_cursor_with_conditions
602
+ dsl {
603
+ puts dedent %Q{
604
+ DECLARE @column_name SYSNAME, @column_sorted_descending BIT;
605
+ DECLARE column_cursor CURSOR FOR
606
+ SELECT c.name, ic.is_descending_key
607
+ FROM sys.key_constraints kc
608
+ INNER JOIN sys.index_columns ic ON kc.parent_object_id = ic.object_id AND kc.unique_index_id = ic.index_id
609
+ INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
610
+ }
611
+ yield
612
+ puts "ORDER BY ic.index_column_id;"
613
+ puts "OPEN column_cursor;"
614
+ }
615
+ end
616
+
617
+ def check_column_sequence_end
618
+ dsl {
619
+ puts dedent %Q{
620
+ FETCH NEXT FROM column_cursor INTO @column_name, @column_sorted_descending;
621
+ IF @@FETCH_STATUS = 0
622
+ }
623
+ puts "BEGIN".."END;" do
624
+ puts error_sql "#{cnstr_id.capitalize} has one or more unexpected columns."
625
+ end
626
+ puts "CLOSE column_cursor;"
627
+ puts "DEALLOCATE column_cursor;"
628
+ }
629
+ end
630
+
631
+ def add_column_sequence_test(index_column)
632
+ dsl {
633
+ puts %Q{
634
+ FETCH NEXT FROM column_cursor INTO @column_name, @column_sorted_descending;
635
+ IF @@FETCH_STATUS <> 0
636
+ }
637
+ puts "BEGIN"
638
+ indented {
639
+ yield "Column #{index_column.name} not found where expected in #{cnstr_id}."
640
+ }
641
+ puts "END ELSE IF NOT (%s)" do
642
+ puts "@column_name = %s" do
643
+ puts strlit(unquoted_identifier index_column.name)
644
+ end
645
+ end
646
+ puts "BEGIN"
647
+ indented {
648
+ yield "Other column found where #{index_column.name} expected in #{cnstr_id}."
649
+ }
650
+ puts "END ELSE IF NOT (%s)" do
651
+ puts bit_test("@column_sorted_descending", index_column.direction == :descending)
652
+ end
653
+ puts "BEGIN"
654
+ indented {
655
+ yield "Column #{index_column.name} should be sorted #{index_column.direction} in #{cnstr_id}."
656
+ }
657
+ puts "END;"
658
+ }
659
+ end
660
+ end
661
+
662
+ def check_primary_key_and_unique_constraints_sql
663
+ db_expectations.pku_constraints.map do |cnstr|
664
+ KeylikeConstraintAdoptionChecks.new(cnstr, method(:adoption_error_sql)).to_s
665
+ end # Do not join -- each needs a separate batch (they use variables)
666
+ end
667
+
668
+ class ForeignKeyAdoptionChecks < IndentedStringBuilder
669
+ include SqlStringManipulators
670
+
671
+ def initialize(keys, error_sql_proc)
672
+ super()
673
+
674
+ @error_sql_proc = error_sql_proc
675
+ @named_keys = keys.reject {|k| k.unnamed?}
676
+ @unnamed_keys = keys.select {|k| k.unnamed?}
677
+
678
+ add_named_key_tests
679
+ add_unnamed_key_tests
680
+ end
681
+
682
+ attr_reader :named_keys, :unnamed_keys
683
+
684
+ def error_sql(s)
685
+ @error_sql_proc.call(s)
686
+ end
687
+
688
+ def add_named_key_tests
689
+ table = 'expected_named_foreign_keys'
690
+ dsl {
691
+ # Create a temporary table
692
+ puts dedent %Q{
693
+ CREATE TABLE [xmigra].[#{table}] (
694
+ [name] NVARCHAR(150) NOT NULL,
695
+ [position] INTEGER NOT NULL,
696
+ [from_table] NVARCHAR(300) NOT NULL,
697
+ [from_column] NVARCHAR(150) NOT NULL,
698
+ [to_table] NVARCHAR(300) NOT NULL,
699
+ [to_column] NVARCHAR(150) NOT NULL
700
+ );
701
+ GO
702
+ }
703
+
704
+ # Insert a record for each column linkage for each named foreign key
705
+ named_keys.each do |fkey|
706
+ fkey.links.each.with_index do |cols, i|
707
+ values = [
708
+ strlit(fkey.name),
709
+ i + 1,
710
+ strlit(fkey.qualified_table),
711
+ strlit(cols[0]),
712
+ strlit(fkey.references.join '.'),
713
+ strlit(cols[1])
714
+ ]
715
+ puts dedent(%Q{
716
+ INSERT INTO [xmigra].[#{table}] (name, position, from_table, from_column, to_table, to_column)
717
+ VALUES (%s);
718
+ } % [values.join(', ')])
719
+ end
720
+ end
721
+
722
+ # Write an adoption error for each missing/misdefined foreign key
723
+ puts dedent %Q{
724
+ WITH
725
+ MissingLinks AS (
726
+ SELECT
727
+ [name],
728
+ [position],
729
+ [from_table],
730
+ [from_column],
731
+ [to_table],
732
+ [to_column]
733
+ FROM [xmigra].[#{table}]
734
+ EXCEPT
735
+ SELECT
736
+ QUOTENAME(fk.name) AS name,
737
+ RANK() OVER(PARTITION BY fk.object_id ORDER BY fkc.constraint_column_id ASC) AS position,
738
+ QUOTENAME(s.name) + N'.' + QUOTENAME(t.name) AS from_table,
739
+ QUOTENAME(from_col.name) AS from_col,
740
+ QUOTENAME(rs.name) + N'.' + QUOTENAME(r.name) AS to_table,
741
+ QUOTENAME(to_col.name) AS to_col
742
+ FROM sys.foreign_keys fk
743
+ JOIN sys.tables t ON fk.parent_object_id = t.object_id
744
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
745
+ JOIN sys.objects r ON fk.referenced_object_id = r.object_id
746
+ JOIN sys.schemas rs ON r.schema_id = rs.schema_id
747
+ JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
748
+ JOIN sys.columns from_col
749
+ ON fk.parent_object_id = from_col.object_id
750
+ AND fkc.parent_column_id = from_col.column_id
751
+ JOIN sys.columns to_col
752
+ ON fk.referenced_object_id = to_col.object_id
753
+ AND fkc.referenced_column_id = to_col.column_id
754
+ )
755
+ INSERT INTO [xmigra].[adoption_errors] ([message])
756
+ SELECT DISTINCT
757
+ N'Constraint ' + ml.[name] + N' on ' + ml.[from_table] + N' (referencing' + ml.[to_table] + N') does not have the expected definition.'
758
+ FROM MissingLinks ml;
759
+ GO
760
+ }
761
+
762
+ # Drop the temporary table
763
+ puts "DROP TABLE [xmigra].[#{table}];\nGO"
764
+ }
765
+ end
766
+
767
+ def add_unnamed_key_tests
768
+ table = 'expected_unnamed_foreign_keys'
769
+ dsl {
770
+ # Create a temporary table
771
+ puts dedent %Q{
772
+ CREATE TABLE [xmigra].[#{table}] (
773
+ [position] INTEGER NOT NULL,
774
+ [from_table] NVARCHAR(300) NOT NULL,
775
+ [from_column] NVARCHAR(150) NOT NULL,
776
+ [to_table] NVARCHAR(300) NOT NULL,
777
+ [to_column] NVARCHAR(150) NOT NULL
778
+ );
779
+ GO
780
+ }
781
+
782
+ # Insert a record for each column linkage for each unnamed foreign key
783
+ unnamed_keys.each do |fkey|
784
+ fkey.links.each.with_index do |cols, i|
785
+ values = [
786
+ i + 1,
787
+ strlit(fkey.qualified_table),
788
+ strlit(cols[0]),
789
+ strlit(fkey.references.join '.'),
790
+ strlit(cols[1])
791
+ ]
792
+ puts dedent(%Q{
793
+ INSERT INTO [xmigra].[#{table}] (position, from_table, from_column, to_table, to_column)
794
+ VALUES (%s);
795
+ } % [values.join(', ')])
796
+ end
797
+ end
798
+
799
+ # Write an adoption error for each missing/misdefined key
800
+ puts dedent %Q{
801
+ WITH
802
+ MissingLinks AS (
803
+ SELECT
804
+ [position],
805
+ [from_table],
806
+ [from_column],
807
+ [to_table],
808
+ [to_column]
809
+ FROM [xmigra].[#{table}]
810
+ EXCEPT
811
+ SELECT
812
+ RANK() OVER(PARTITION BY fk.object_id ORDER BY fkc.constraint_column_id ASC) AS position,
813
+ QUOTENAME(s.name) + N'.' + QUOTENAME(t.name) AS from_table,
814
+ QUOTENAME(from_col.name) AS from_col,
815
+ QUOTENAME(rs.name) + N'.' + QUOTENAME(r.name) AS to_table,
816
+ QUOTENAME(to_col.name) AS to_col
817
+ FROM sys.foreign_keys fk
818
+ JOIN sys.tables t ON fk.parent_object_id = t.object_id
819
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
820
+ JOIN sys.objects r ON fk.referenced_object_id = r.object_id
821
+ JOIN sys.schemas rs ON r.schema_id = rs.schema_id
822
+ JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
823
+ JOIN sys.columns from_col
824
+ ON fk.parent_object_id = from_col.object_id
825
+ AND fkc.parent_column_id = from_col.column_id
826
+ JOIN sys.columns to_col
827
+ ON fk.referenced_object_id = to_col.object_id
828
+ AND fkc.referenced_column_id = to_col.column_id
829
+ )
830
+ INSERT INTO [xmigra].[adoption_errors] ([message])
831
+ SELECT DISTINCT
832
+ N'Expected constraint on ' + ml.[from_table] + N' (referencing ' + ml.[to_table] + N') not found.'
833
+ FROM MissingLinks ml;
834
+ GO
835
+ }
836
+
837
+ # Drop the temporary table
838
+ puts "DROP TABLE [xmigra].[#{table}];\nGO"
839
+ }
840
+ end
841
+ end
842
+
843
+ def check_foreign_key_constraints_sql
844
+ ForeignKeyAdoptionChecks.new(db_expectations.foreign_keys, method(:adoption_error_sql)).to_s
845
+ end
846
+
847
+ class CheckConstraintAdoptionChecks < IndentedStringBuilder
848
+ include SqlStringManipulators
849
+
850
+ def initialize(cnstr, error_sql_proc)
851
+ super()
852
+
853
+ @cnstr = cnstr
854
+ @cnstr_id = "check constraint%s on #{cnstr.qualified_table}" % [
855
+ cnstr.name ? cnstr.name + " " : ""
856
+ ]
857
+ @error_sql_proc = error_sql_proc
858
+
859
+ @schema_name = unquoted_identifier cnstr.schema
860
+ @table_name = unquoted_identifier cnstr.table
861
+ @cnstr_name = unquoted_identifier(cnstr.name) if cnstr.name
862
+
863
+ if cnstr.name
864
+ add_named_constraint_tests
865
+ else
866
+ add_unnamed_constraint_tests
867
+ end
868
+ end
869
+
870
+ attr_reader :cnstr, :cnstr_id, :schema_name, :table_name
871
+
872
+ def error_sql(s)
873
+ @error_sql_proc.call(s)
874
+ end
875
+
876
+ def add_named_constraint_tests
877
+ dsl {
878
+ puts "IF NOT EXISTS (%s)" do
879
+ puts dedent %Q{
880
+ SELECT * FROM sys.check_constraints cc
881
+ INNER JOIN sys.tables t ON cc.object_id = t.object_id
882
+ INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
883
+ WHERE s.name = #{strlit schema_name}
884
+ AND t.name = #{strlit table_name}
885
+ AND cc.name = #{strlit cnstr_name}
886
+ }
887
+ end
888
+ puts "BEGIN"
889
+ indented {
890
+ puts error_sql "#{cnstr_id.capitalize} does not exist."
891
+ }
892
+ puts "END ELSE IF NOT EXISTS (%s)" do
893
+ puts dedent %Q{
894
+ SELECT * FROM sys.check_constraints cc
895
+ INNER JOIN sys.tables t ON cc.parent_object_id = t.object_id
896
+ INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
897
+ WHERE s.name = #{strlit schema_name}
898
+ AND t.name = #{strlit table_name}
899
+ AND cc.name = #{strlit cnstr_name}
900
+ AND cc.definition = #{strlit cnstr.definition}
901
+ }
902
+ end
903
+ puts "BEGIN"
904
+ indented {
905
+ puts error_sql "#{cnstr_id.capitalize} does not have expected definition."
906
+ }
907
+ puts "END;"
908
+ }
909
+ end
910
+
911
+ def add_unnamed_constraint_tests
912
+ dsl {
913
+ puts "IF NOT EXISTS (%s)" do
914
+ puts dedent %Q{
915
+ SELECT cc.object_id
916
+ FROM sys.check_constraints cc
917
+ INNER JOIN sys.tables t ON cc.parent_object_id = t.object_id
918
+ INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
919
+ WHERE s.name = #{strlit schema_name}
920
+ AND t.name = #{strlit table_name}
921
+ AND cc.definition = #{strlit cnstr.definition}
922
+ }
923
+ end
924
+ puts "BEGIN".."END;" do
925
+ puts error_sql "Expected #{cnstr_id} does not exist."
926
+ end
927
+ }
928
+ end
929
+ end
930
+
931
+ def check_check_constraints_sql
932
+ db_expectations.check_constraints.map do |cnstr|
933
+ CheckConstraintAdoptionChecks.new(cnstr, method(:adoption_error_sql)).to_s
934
+ end # Do not join -- each needs a separate batch (they use variables)
935
+ end
936
+
937
+ class IndexAdoptionChecks < IndentedStringBuilder
938
+ include SqlStringManipulators
939
+
940
+ def initialize(index, error_sql_proc)
941
+ super()
942
+
943
+ @index = index
944
+ @error_sql_proc = error_sql_proc
945
+
946
+ @index_id = "index #{@index.name} on #{@index.qualified_relation}"
947
+
948
+ dsl {
949
+ puts "DECLARE @relation_id INT, @index_id INT;"
950
+ puts dedent %Q{
951
+ SELECT @relation_id = i.object_id, @index_id = i.index_id
952
+ FROM sys.indexes i
953
+ JOIN sys.objects rel ON i.object_id = rel.object_id
954
+ JOIN sys.schemas s ON rel.schema_id = s.schema_id
955
+ WHERE s.name = #{strlit(unquoted_identifier index.schema)}
956
+ AND rel.name = #{strlit(unquoted_identifier index.relation)}
957
+ AND i.name = #{strlit(unquoted_identifier index.name)}
958
+ }
959
+ puts "IF @index_id IS NULL"
960
+ puts "BEGIN"
961
+ indented {
962
+ puts error_sql "#{index_id.capitalize} does not exist."
963
+ }
964
+ puts "END ELSE BEGIN"
965
+ indented {
966
+ add_index_property_checks
967
+ }
968
+ puts "END"
969
+ }
970
+ end
971
+
972
+ attr_reader :index, :index_id
973
+
974
+ def error_sql(s)
975
+ @error_sql_proc.call(s)
976
+ end
977
+
978
+ def add_index_property_checks
979
+ dsl {
980
+ puts property_verification("is_unique", index.unique?, "be unique")
981
+ puts property_verification("ignore_dup_key", index.ignore_duplicates?, "ignore duplicate keys")
982
+
983
+ # Key columns
984
+ QueryCursor.new(
985
+ dedent(%Q{
986
+ SELECT c.column_name, ic.is_descending_key
987
+ FROM sys.index_columns ic
988
+ JOIN sys.columns c
989
+ ON ic.object_id = c.object_id
990
+ AND ic.column_id = c.column_id
991
+ WHERE ic.object_id = @relation_id
992
+ AND ic.index_id = @index_id
993
+ AND ic.key_ordinal >= 1
994
+ ORDER BY ic.key_ordinal
995
+ }),
996
+ "@column_name SYSNAME, @is_sorted_descending BIT",
997
+ output_to: self
998
+ ).expectations(
999
+ on_extra: ->{puts error_sql "#{index_id.capitalize} has one or more unexpected key columns."}
1000
+ ) do |test|
1001
+ index.columns.each.with_index do |column, i|
1002
+ test.row(
1003
+ on_missing: ->{puts error_sql "#{index_id.capitalize} is missing expected column #{column.name}."}
1004
+ ) {
1005
+ puts "IF QUOTENAME(@column_name) <> #{strlit column.name}"
1006
+ puts "BEGIN"
1007
+ indented {
1008
+ puts error_sql "Expected #{column.name} as column #{i + 1} in #{index_id}."
1009
+ }
1010
+ puts "END ELSE IF #{bit_test('@is_sorted_descending', column.direction == :descending)}"
1011
+ indented {
1012
+ puts error_sql "Expected #{column.name} to be sorted #{column.direction} in #{index_id}."
1013
+ }
1014
+ puts "END;"
1015
+ }
1016
+ end
1017
+ end
1018
+
1019
+ # 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."
1033
+ end
1034
+ }
1035
+
1036
+ add_spatial_property_checks(index) if index.spatial_index_geometry
1037
+ end
1038
+
1039
+ def index_property_check(expectation, expectation_desc)
1040
+ %Q{
1041
+ IF NOT EXIST (
1042
+ SELECT * FROM sys.indexes i
1043
+ WHERE i.object_id = @relation_id
1044
+ AND i.index_id = @index_id
1045
+ AND i.#{expectation}
1046
+ )
1047
+ BEGIN
1048
+ #{error_sql "#{@index_id.capitalize} should #{expectation_desc}."}
1049
+ END;
1050
+ }.strip.gsub(/\s+/, ' ')
1051
+ end
1052
+
1053
+ def property_verification(f, v, d)
1054
+ index_property_check(bit_test(f, v), boolean_desc(v, d))
1055
+ end
1056
+ end
1057
+
1058
+ def adopt_indexes_sql
1059
+ db_expectations.indexes.map do |index|
1060
+ index_builder = @xn_builder.indexes[index.name]
1061
+ IndexAdoptionChecks.new(index, method(:adoption_error_sql)).to_s +
1062
+ "\nINSERT INTO [xmigra].[indexes] ([IndexID], [name]) VALUES (%s, %s);" % [
1063
+ index_builder.id, index_builder.name
1064
+ ].map {|s| strlit(s)}
1065
+ end
1066
+ end
1067
+
1068
+ class StatisticsAdoptionChecks < IndentedStringBuilder
1069
+ include SqlStringManipulators
1070
+
1071
+ def initialize(statistics, error_sql_proc)
1072
+ super()
1073
+
1074
+ @statistics = statistics
1075
+ @error_sql_proc = error_sql_proc
1076
+
1077
+ @stats_id = "statistics #{statistics.name} on #{statistics.qualified_relation}"
1078
+
1079
+ dsl {
1080
+ puts "IF NOT EXISTS (%s)" do
1081
+ puts dedent %Q{
1082
+ SELECT * FROM sys.stats so
1083
+ INNER JOIN sys.objects rel ON so.object_id = rel.object_id
1084
+ INNER JOIN sys.schemas s ON rel.schema_id = s.schema_id
1085
+ WHERE s.name = #{strlit(unquoted_identifier statistics.schema)}
1086
+ AND rel.name = #{strlit(unquoted_identifier statistics.relation)}
1087
+ AND so.name = #{strlit(unquoted_identifier statistics.name)}
1088
+ }
1089
+ end
1090
+ puts "BEGIN"
1091
+ indented {
1092
+ puts error_sql "#{stats_id.capitalize} does not exist."
1093
+ }
1094
+ puts "END ELSE BEGIN"
1095
+ indented {
1096
+ # Check column sequence
1097
+ QueryCursor.new(
1098
+ dedent(%Q{
1099
+ SELECT c.name
1100
+ FROM sys.stats so
1101
+ JOIN sys.stats_columns sc
1102
+ ON so.object_id = sc.object_id
1103
+ AND so.stats_id = sc.stats_id
1104
+ JOIN sys.columns c
1105
+ ON sc.object_id = c.object_id
1106
+ AND sc.column_id = c.column_id
1107
+ JOIN sys.objects rel ON so.object_id = rel.object_id
1108
+ JOIN sys.schemas s ON rel.schema_id = s.schema_id
1109
+ WHERE s.name = #{strlit(unquoted_identifier statistics.schema)}
1110
+ AND rel.name = #{strlit(unquoted_identifier statistics.relation)}
1111
+ AND so.name = #{strlit(unquoted_identifier statistics.name)}
1112
+ ORDER BY sc.stats_column_id
1113
+ }),
1114
+ "@column_name SYSNAME",
1115
+ output_to: self
1116
+ ).expectations(
1117
+ on_extra: ->{puts error_sql "#{stats_id.capitalize} has one or more unexpected columns."},
1118
+ ) do |test|
1119
+ statistics.columns.each.with_index do |col_name, i|
1120
+ test.row(
1121
+ on_missing: ->{puts error_sql "#{stats_id.capitalize} is missing #{col_name}."},
1122
+ ) {
1123
+ puts "IF QUOTENAME(@column_name) <> #{strlit col_name}"
1124
+ puts "BEGIN".."END" do
1125
+ puts error_sql "Expected #{col_name} as column #{i + 1} of #{stats_id}."
1126
+ end
1127
+ }
1128
+ end
1129
+ end
1130
+ }
1131
+ puts "END;"
1132
+ }
1133
+ end
1134
+
1135
+ attr_reader :stats_id
1136
+
1137
+ def error_sql(s)
1138
+ @error_sql_proc.call(s)
1139
+ end
1140
+ end
1141
+
1142
+ def adopt_statistics_sql
1143
+ db_expectations.statistics.map do |statistics|
1144
+ StatisticsAdoptionChecks.new(statistics, method(:adoption_error_sql)).to_s +
1145
+ "\nINSERT INTO [xmigra].[statistics] ([Name], [Columns]) VALUES (%s, %s);" % [
1146
+ statistics.name,
1147
+ statistics.columns.join(', ')
1148
+ ].map {|s| strlit(s)}
1149
+ end
1150
+ end
1151
+
1152
+ def access_object_adoption_sql(type, qualified_name)
1153
+ "INSERT INTO [xmigra].[access_objects] ([type], [name]) VALUES (N'#{type}', #{strlit qualified_name});"
1154
+ end
1155
+
1156
+ def definition_matches_by_hash(expr, definition)
1157
+ "HASHBYTES('md5', #{expr}) = 0x#{Digest::MD5.hexdigest definition.gsub("\n", "\r\n").encode('UTF-16LE')}"
1158
+ end
1159
+
1160
+ def adopt_views_sql
1161
+ db_expectations.views.map do |view|
1162
+ IndentedStringBuilder.dsl {
1163
+ puts "IF NOT EXISTS (%s)" do
1164
+ puts dedent %Q{
1165
+ SELECT * FROM sys.views v
1166
+ JOIN sys.schemas s ON v.schema_id = s.schema_id
1167
+ WHERE s.name = #{strlit(unquoted_identifier view.schema)}
1168
+ AND v.name = #{strlit(unquoted_identifier view.name)}
1169
+ }
1170
+ end
1171
+ puts "BEGIN"
1172
+ indented do
1173
+ puts adoption_error_sql "View #{view.qualified_name} does not exist."
1174
+ end
1175
+ puts "END ELSE IF NOT EXISTS (%s)" do
1176
+ puts dedent %Q{
1177
+ SELECT * FROM sys.views v
1178
+ JOIN sys.schemas s ON v.schema_id = s.schema_id
1179
+ JOIN sys.sql_modules sql ON v.object_id = sql.object_id
1180
+ WHERE s.name = #{strlit(unquoted_identifier view.schema)}
1181
+ AND v.name = #{strlit(unquoted_identifier view.name)}
1182
+ AND #{definition_matches_by_hash 'sql.definition', view.definition}
1183
+ }
1184
+ end
1185
+ puts "BEGIN"
1186
+ indented {
1187
+ puts adoption_error_sql "View #{view.qualified_name} does not have the expected definition."
1188
+ }
1189
+ puts "END;"
1190
+ puts access_object_adoption_sql(:VIEW, view.qualified_name)
1191
+ }
1192
+ end
1193
+ end
1194
+
1195
+ def adopt_stored_procedures_sql
1196
+ db_expectations.procedures.map do |sproc|
1197
+ IndentedStringBuilder.dsl {
1198
+ puts "IF NOT EXISTS (%s)" do
1199
+ puts dedent %Q{
1200
+ SELECT * FROM sys.procedures p
1201
+ JOIN sys.schemas s ON p.schema_id = s.schema_id
1202
+ WHERE s.name = #{strlit(unquoted_identifier sproc.schema)}
1203
+ AND p.name = #{strlit(unquoted_identifier sproc.name)}
1204
+ }
1205
+ end
1206
+ puts "BEGIN"
1207
+ indented {
1208
+ puts adoption_error_sql "Stored procedure #{sproc.qualified_name} does not exist."
1209
+ }
1210
+ 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
+ }
1219
+ end
1220
+ puts "BEGIN"
1221
+ indented {
1222
+ puts adoption_error_sql "Stored procedure #{sproc.qualified_name} does not have the expected definition."
1223
+ }
1224
+ puts "END;"
1225
+ puts access_object_adoption_sql(:PROCEDURE, sproc.qualified_name)
1226
+ }
1227
+ end
1228
+ end
1229
+
1230
+ def adopt_udfs_sql
1231
+ db_expectations.udfs.map do |udf|
1232
+ IndentedStringBuilder.dsl {
1233
+ puts "IF NOT EXISTS (%s)" do
1234
+ puts dedent %Q{
1235
+ SELECT * FROM sys.objects fn
1236
+ JOIN sys.schemas s ON fn.schema_id = s.schema_id
1237
+ WHERE s.name = #{strlit(unquoted_identifier udf.schema)}
1238
+ AND fn.name = #{strlit(unquoted_identifier udf.name)}
1239
+ AND fn.type IN ('FN', 'IF', 'TF')
1240
+ }
1241
+ end
1242
+ puts "BEGIN"
1243
+ indented {
1244
+ puts adoption_error_sql "Function #{udf.qualified_name} does not exist."
1245
+ }
1246
+ 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
+ }
1255
+ end
1256
+ puts "BEGIN"
1257
+ indented {
1258
+ puts adoption_error_sql "Function #{udf.qualified_name} does not have the expected definition."
1259
+ }
1260
+ puts "END;"
1261
+ puts access_object_adoption_sql(:FUNCTION, udf.qualified_name)
1262
+ }
1263
+ end
1264
+ end
1265
+
1266
+ def adopt_permissions_sql
1267
+ table = 'expected_permissions'
1268
+ [
1269
+ # Create a temporary table
1270
+ dedent(%Q{
1271
+ CREATE TABLE [xmigra].[#{table}] (
1272
+ [state] CHAR(1) NOT NULL,
1273
+ [subject] NVARCHAR(150) NOT NULL,
1274
+ [permission] NVARCHAR(128) NOT NULL,
1275
+ [object_type] NVARCHAR(25) NOT NULL,
1276
+ [object_schema] NVARCHAR(150) NULL,
1277
+ [object] NVARCHAR(150) NULL,
1278
+ [column] NVARCHAR(150) NULL
1279
+ );
1280
+ }),
1281
+ # Insert permission rows into the table
1282
+ [].tap do |inserts|
1283
+ db_expectations.permissions.each do |pg|
1284
+ pg.permissions.each do |pmsn|
1285
+ state = case pg.action[0].downcase
1286
+ when 'g' then pmsn.grant_option? ? 'W' : 'G'
1287
+ else pg.action[0].upcase
1288
+ end
1289
+ nls = ->(s) {s.nil? ? 'NULL' : strlit(s)}
1290
+ row_values = [state, pg.subject, pmsn.name] + pmsn.object_id_parts
1291
+ inserts << dedent(%Q{
1292
+ INSERT INTO [xmigra].[#{table}] (state, subject, permission, object_type, object_schema, object, [column])
1293
+ VALUES (%s);
1294
+ } % row_values.map(&nls).join(', '))
1295
+ end
1296
+ end
1297
+ end.join("\n"),
1298
+ # Write an adoption error for each missing permission
1299
+ dedent(%Q{
1300
+ WITH
1301
+ PermissionTarget AS (
1302
+ SELECT
1303
+ 0 AS "class",
1304
+ 0 AS major_id,
1305
+ 0 AS minor_id,
1306
+ 'DATABASE' AS "class_desc",
1307
+ NULL AS "class_specifier",
1308
+ NULL AS "schema_name",
1309
+ NULL AS "object_name",
1310
+ NULL AS "column_name"
1311
+ UNION
1312
+ SELECT
1313
+ 1,
1314
+ o.object_id,
1315
+ 0,
1316
+ 'OBJECT',
1317
+ NULL,
1318
+ s.name,
1319
+ o.name,
1320
+ NULL
1321
+ FROM sys.objects o
1322
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
1323
+ UNION
1324
+ SELECT
1325
+ 1,
1326
+ c.object_id,
1327
+ c.column_id,
1328
+ 'COLUMN',
1329
+ NULL,
1330
+ s.name,
1331
+ o.name,
1332
+ c.name
1333
+ FROM sys.columns c
1334
+ JOIN sys.objects o ON c.object_id = o.object_id
1335
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
1336
+ UNION
1337
+ SELECT
1338
+ 3,
1339
+ s.schema_id,
1340
+ 0,
1341
+ 'SCHEMA',
1342
+ 'SCHEMA',
1343
+ NULL,
1344
+ s.name,
1345
+ NULL
1346
+ FROM sys.schemas s
1347
+ UNION
1348
+ SELECT
1349
+ 4, -- class
1350
+ r.principal_id, -- major_id
1351
+ 0, -- minor_id
1352
+ 'ROLE', -- class description
1353
+ 'ROLE', -- class specifier
1354
+ NULL, -- schema_name
1355
+ r.name, -- object_name
1356
+ NULL -- column_name
1357
+ FROM sys.database_principals r
1358
+ WHERE r.type = 'R'
1359
+ UNION
1360
+ SELECT
1361
+ 5, -- class
1362
+ a.assembly_id, -- major_id
1363
+ 0, -- minor_id
1364
+ 'ASSEMBLY', -- class description
1365
+ 'ASSEMBLY', -- class specifier
1366
+ NULL, -- schema_name
1367
+ a.name, -- object_name
1368
+ NULL -- column_name
1369
+ FROM sys.assemblies a
1370
+ UNION
1371
+ SELECT
1372
+ 6, -- class
1373
+ t.user_type_id, -- major_id
1374
+ 0, -- minor_id
1375
+ 'TYPE', -- class description
1376
+ 'TYPE', -- class specifier
1377
+ s.name, -- schema_name
1378
+ t.name, -- object_name
1379
+ NULL -- column_name
1380
+ FROM sys.types t
1381
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
1382
+ UNION
1383
+ SELECT
1384
+ 10, -- class
1385
+ xsc.xml_collection_id, -- major_id
1386
+ 0, -- minor_id
1387
+ 'XML_SCHEMA_COLLECTION', -- class description
1388
+ 'XML SCHEMA COLLECTION', -- class specifier
1389
+ s.name, -- schema_name
1390
+ xsc.name, -- object_name
1391
+ NULL -- column_name
1392
+ FROM sys.xml_schema_collections xsc
1393
+ JOIN sys.schemas s ON xsc.schema_id = s.schema_id
1394
+ ),
1395
+ Permissions AS (
1396
+ SELECT
1397
+ p.state,
1398
+ QUOTENAME(pi.name) AS "subject",
1399
+ p.permission_name AS "permission",
1400
+ t.class_desc AS "object_type",
1401
+ QUOTENAME(t.schema_name) AS "object_schema",
1402
+ QUOTENAME(t.object_name) AS "object",
1403
+ QUOTENAME(t.column_name) AS "column"
1404
+ FROM sys.database_permissions p
1405
+ JOIN sys.database_principals pi ON p.grantee_principal_id = pi.principal_id
1406
+ JOIN PermissionTarget t
1407
+ ON p.class = t.class
1408
+ AND p.major_id = t.major_id
1409
+ AND p.minor_id = t.minor_id
1410
+ LEFT JOIN sys.database_principals grantor ON p.grantor_principal_id = grantor.principal_id
1411
+ AND (p.class <> 4 OR (
1412
+ SELECT dp.type FROM sys.database_principals dp
1413
+ WHERE dp.principal_id = p.major_id
1414
+ ) = 'R')
1415
+ AND (p.class <> 1 OR p.major_id IN (
1416
+ SELECT o.object_id FROM sys.objects o
1417
+ ))
1418
+ )
1419
+ INSERT INTO [xmigra].[adoption_errors] ([message])
1420
+ SELECT
1421
+ e.permission + N' is ' +
1422
+ CASE e.state
1423
+ WHEN 'G' THEN
1424
+ CASE (
1425
+ SELECT p.state
1426
+ FROM Permissions p
1427
+ WHERE p.subject = e.subject
1428
+ AND p.permission = e.permission
1429
+ AND p.object_type = e.object_type
1430
+ AND COALESCE(p.object_schema, N'.') = COALESCE(e.object_schema, N'.')
1431
+ AND COALESCE(p.object, N'.') = COALESCE(e.object, N'.')
1432
+ AND COALESCE(p.[column], N'.') = COALESCE(e.[column], N'.')
1433
+ )
1434
+ WHEN 'W' THEN 'GRANTed with (unexpected) grant option to '
1435
+ ELSE N'not GRANTed to '
1436
+ END
1437
+ WHEN 'W' THEN N'not GRANTed (with grant option) to '
1438
+ WHEN 'D' THEN N'not DENYed to '
1439
+ WHEN 'R' THEN N'not REVOKEd from '
1440
+ END + e.subject + N' on ' + e.object_type +
1441
+ CASE
1442
+ WHEN e.object_schema IS NULL THEN N''
1443
+ ELSE e.object_schema + N'.'
1444
+ END +
1445
+ CASE
1446
+ WHEN e.object IS NULL THEN N''
1447
+ ELSE e.object
1448
+ END +
1449
+ CASE
1450
+ WHEN e.[column] IS NULL THEN N''
1451
+ ELSE N' (' + e.[column] + N')'
1452
+ END + N'.'
1453
+ FROM (
1454
+ SELECT state, subject, permission, object_type, object_schema, object, [column]
1455
+ FROM [xmigra].[#{table}]
1456
+ EXCEPT
1457
+ SELECT
1458
+ state COLLATE SQL_Latin1_General_CP1_CI_AS,
1459
+ subject,
1460
+ permission COLLATE SQL_Latin1_General_CP1_CI_AS,
1461
+ object_type,
1462
+ object_schema,
1463
+ object,
1464
+ [column]
1465
+ FROM Permissions
1466
+ ) e
1467
+ }),
1468
+ # Record adopted permissions
1469
+ db_expectations.permissions.map do |pg|
1470
+ pg.regular_permissions.map do |pmsn|
1471
+ "EXEC [xmigra].[ip_prepare_revoke] #{[pmsn.name, pmsn.target, pg.subject].map {|s| strlit(unquoted_identifier s)}.join(', ')};"
1472
+ end
1473
+ end.flatten.join("\n"),
1474
+ # Drop the temporary table
1475
+ "DROP TABLE [xmigra].[#{table}];",
1476
+ ]
1477
+ end
1478
+
1479
+ def write_version_bridge_record_sql
1480
+ dedent %Q{
1481
+ INSERT INTO [xmigra].[applied] ([MigrationID], [VersionBridgeMark], [Description])
1482
+ VALUES (#{strlit @xn_builder.migrations.last.id}, 1, N'Adoption of existing structure.');
1483
+ }
1484
+ end
1485
+ end
1486
+ end