mkxms-mssql 1.0.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 (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