xmigra 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1070 @@
1
+
2
+ module XMigra
3
+ module MSSQLSpecifics
4
+ DatabaseSupportModules << self
5
+
6
+ SYSTEM_NAME = 'Microsoft SQL Server'
7
+
8
+ IDENTIFIER_SUBPATTERN = '[a-z_@#][a-z0-9@$#_]*|"[^\[\]"]+"|\[[^\[\]]+\]'
9
+ DBNAME_PATTERN = /^
10
+ (?:(#{IDENTIFIER_SUBPATTERN})\.)?
11
+ (#{IDENTIFIER_SUBPATTERN})
12
+ $/ix
13
+ STATISTICS_FILE = 'statistics-objects.yaml'
14
+
15
+ class StatisticsObject
16
+ def initialize(name, params)
17
+ (@name = name.dup).freeze
18
+ (@target = params[0].dup).freeze
19
+ (@columns = params[1].dup).freeze
20
+ @options = params[2] || {}
21
+ @options.freeze
22
+ @options.each_value {|v| v.freeze}
23
+ end
24
+
25
+ attr_reader :name, :target, :columns, :options
26
+
27
+ def creation_sql
28
+ result = "CREATE STATISTICS #{name} ON #{target} (#{columns})"
29
+
30
+ result += " WHERE " + @options['where'] if @options['where']
31
+ result += " WITH " + @options['with'] if @options['with']
32
+
33
+ result += ";"
34
+ return result
35
+ end
36
+ end
37
+
38
+ def ddl_block_separator; "\nGO\n"; end
39
+ def filename_metavariable; "[{filename}]"; end
40
+
41
+ def stats_objs
42
+ return @stats_objs if @stats_objs
43
+
44
+ begin
45
+ stats_data = YAML::load_file(path.join(MSSQLSpecifics::STATISTICS_FILE))
46
+ rescue Errno::ENOENT
47
+ return @stats_objs = [].freeze
48
+ end
49
+
50
+ @stats_objs = stats_data.collect {|item| StatisticsObject.new(*item)}
51
+ @stats_objs.each {|o| o.freeze}
52
+ @stats_objs.freeze
53
+
54
+ return @stats_objs
55
+ end
56
+
57
+ def in_ddl_transaction
58
+ parts = []
59
+ parts << <<-"END_OF_SQL"
60
+ SET ANSI_NULLS ON
61
+ GO
62
+
63
+ SET QUOTED_IDENTIFIER ON
64
+ GO
65
+
66
+ SET ANSI_PADDING ON
67
+ GO
68
+
69
+ SET NOCOUNT ON
70
+ GO
71
+
72
+ BEGIN TRY
73
+ BEGIN TRAN;
74
+ END_OF_SQL
75
+
76
+ each_batch(yield) do |batch|
77
+ batch_literal = MSSQLSpecifics.string_literal("\n" + batch)
78
+ parts << "EXEC sp_executesql @statement = #{batch_literal};"
79
+ end
80
+
81
+ parts << <<-"END_OF_SQL"
82
+ COMMIT TRAN;
83
+ END TRY
84
+ BEGIN CATCH
85
+ ROLLBACK TRAN;
86
+
87
+ DECLARE @ErrorMessage NVARCHAR(4000);
88
+ DECLARE @ErrorSeverity INT;
89
+ DECLARE @ErrorState INT;
90
+
91
+ PRINT N'Update failed: ' + ERROR_MESSAGE();
92
+ PRINT N' State: ' + CAST(ERROR_STATE() AS NVARCHAR);
93
+ PRINT N' Line: ' + CAST(ERROR_LINE() AS NVARCHAR);
94
+
95
+ SELECT
96
+ @ErrorMessage = N'Update failed: ' + ERROR_MESSAGE(),
97
+ @ErrorSeverity = ERROR_SEVERITY(),
98
+ @ErrorState = ERROR_STATE();
99
+
100
+ -- Use RAISERROR inside the CATCH block to return error
101
+ -- information about the original error that caused
102
+ -- execution to jump to the CATCH block.
103
+ RAISERROR (@ErrorMessage, -- Message text.
104
+ @ErrorSeverity, -- Severity.
105
+ @ErrorState -- State.
106
+ );
107
+ END CATCH;
108
+ END_OF_SQL
109
+
110
+ return parts.join("\n")
111
+ end
112
+
113
+ def amend_script_parts(parts)
114
+ parts.insert_after(
115
+ :create_and_fill_indexes_table_sql,
116
+ :create_and_fill_statistics_table_sql
117
+ )
118
+ parts.insert_after(
119
+ :remove_undesired_indexes_sql,
120
+ :remove_undesired_statistics_sql
121
+ )
122
+ parts.insert_after(:create_new_indexes_sql, :create_new_statistics_sql)
123
+ end
124
+
125
+ def check_execution_environment_sql
126
+ <<-"END_OF_SQL"
127
+ PRINT N'Checking execution environment:';
128
+ IF DB_NAME() IN ('master', 'tempdb', 'model', 'msdb')
129
+ BEGIN
130
+ RAISERROR(N'Please select an appropriate target database for the update script.', 11, 1);
131
+ END;
132
+ END_OF_SQL
133
+ end
134
+
135
+ def ensure_version_tables_sql
136
+ <<-"END_OF_SQL"
137
+ PRINT N'Ensuring version tables:';
138
+ IF NOT EXISTS (
139
+ SELECT * FROM sys.schemas
140
+ WHERE name = N'xmigra'
141
+ )
142
+ BEGIN
143
+ EXEC sp_executesql N'
144
+ CREATE SCHEMA [xmigra] AUTHORIZATION [dbo];
145
+ ';
146
+ END;
147
+ GO
148
+
149
+ IF NOT EXISTS (
150
+ SELECT * FROM sys.objects
151
+ WHERE object_id = OBJECT_ID(N'[xmigra].[applied]')
152
+ AND type IN (N'U')
153
+ )
154
+ BEGIN
155
+ CREATE TABLE [xmigra].[applied] (
156
+ [MigrationID] nvarchar(80) NOT NULL,
157
+ [ApplicationOrder] int IDENTITY(1,1) NOT NULL,
158
+ [VersionBridgeMark] bit NOT NULL,
159
+ [Description] nvarchar(max) NOT NULL,
160
+
161
+ CONSTRAINT [PK_version] PRIMARY KEY CLUSTERED (
162
+ [MigrationID] ASC
163
+ ) WITH (
164
+ PAD_INDEX = OFF,
165
+ STATISTICS_NORECOMPUTE = OFF,
166
+ IGNORE_DUP_KEY = OFF,
167
+ ALLOW_ROW_LOCKS = ON,
168
+ ALLOW_PAGE_LOCKS = ON
169
+ ) ON [PRIMARY]
170
+ ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY];
171
+ END;
172
+ GO
173
+
174
+ IF NOT EXISTS (
175
+ SELECT * FROM sys.objects
176
+ WHERE object_id = OBJECT_ID(N'[xmigra].[DF_version_VersionBridgeMark]')
177
+ AND type IN (N'D')
178
+ )
179
+ BEGIN
180
+ ALTER TABLE [xmigra].[applied] ADD CONSTRAINT [DF_version_VersionBridgeMark]
181
+ DEFAULT (0) FOR [VersionBridgeMark];
182
+ END;
183
+ GO
184
+
185
+ IF NOT EXISTS (
186
+ SELECT * FROM sys.objects
187
+ WHERE object_id = OBJECT_ID(N'[xmigra].[access_objects]')
188
+ AND type IN (N'U')
189
+ )
190
+ BEGIN
191
+ CREATE TABLE [xmigra].[access_objects] (
192
+ [type] nvarchar(40) NOT NULL,
193
+ [name] nvarchar(256) NOT NULL,
194
+ [order] int identity(1,1) NOT NULL,
195
+
196
+ CONSTRAINT [PK_access_objects] PRIMARY KEY CLUSTERED (
197
+ [name] ASC
198
+ ) WITH (
199
+ PAD_INDEX = OFF,
200
+ STATISTICS_NORECOMPUTE = OFF,
201
+ IGNORE_DUP_KEY = OFF,
202
+ ALLOW_ROW_LOCKS = ON,
203
+ ALLOW_PAGE_LOCKS = ON
204
+ ) ON [PRIMARY]
205
+ ) ON [PRIMARY];
206
+ END;
207
+ GO
208
+
209
+ IF NOT EXISTS (
210
+ SELECT * FROM sys.objects
211
+ WHERE object_id = OBJECT_ID(N'[xmigra].[indexes]')
212
+ AND type in (N'U')
213
+ )
214
+ BEGIN
215
+ CREATE TABLE [xmigra].[indexes] (
216
+ [IndexID] nvarchar(80) NOT NULL PRIMARY KEY,
217
+ [name] nvarchar(256) NOT NULL
218
+ ) ON [PRIMARY];
219
+ END;
220
+
221
+ IF NOT EXISTS (
222
+ SELECT * FROM sys.objects
223
+ WHERE object_id = OBJECT_ID(N'[xmigra].[statistics]')
224
+ AND type in (N'U')
225
+ )
226
+ BEGIN
227
+ CREATE TABLE [xmigra].[statistics] (
228
+ [Name] nvarchar(100) NOT NULL PRIMARY KEY,
229
+ [Columns] nvarchar(256) NOT NULL
230
+ ) ON [PRIMARY];
231
+ END;
232
+
233
+ IF NOT EXISTS (
234
+ SELECT * FROM sys.objects
235
+ WHERE object_id = OBJECT_ID(N'[xmigra].[branch_upgrade]')
236
+ AND type in (N'U')
237
+ )
238
+ BEGIN
239
+ CREATE TABLE [xmigra].[branch_upgrade] (
240
+ [ApplicationOrder] int identity(1,1) NOT NULL,
241
+ [Current] nvarchar(80) NOT NULL PRIMARY KEY,
242
+ [Next] nvarchar(80) NULL,
243
+ [UpgradeSql] nvarchar(max) NULL,
244
+ [CompletesMigration] nvarchar(80) NULL
245
+ ) ON [PRIMARY];
246
+ END;
247
+ END_OF_SQL
248
+ end
249
+
250
+ def create_and_fill_migration_table_sql
251
+ intro = <<-"END_OF_SQL"
252
+ IF EXISTS (
253
+ SELECT * FROM sys.objects
254
+ WHERE object_id = OBJECT_ID(N'[xmigra].[migrations]')
255
+ AND type IN (N'U')
256
+ )
257
+ BEGIN
258
+ DROP TABLE [xmigra].[migrations];
259
+ END;
260
+ GO
261
+
262
+ CREATE TABLE [xmigra].[migrations] (
263
+ [MigrationID] nvarchar(80) NOT NULL,
264
+ [ApplicationOrder] int NOT NULL,
265
+ [Description] ntext NOT NULL,
266
+ [Install] bit NOT NULL DEFAULT(0)
267
+ );
268
+ GO
269
+
270
+ END_OF_SQL
271
+
272
+ mig_insert = <<-"END_OF_SQL"
273
+ INSERT INTO [xmigra].[migrations] (
274
+ [MigrationID],
275
+ [ApplicationOrder],
276
+ [Description]
277
+ ) VALUES
278
+ END_OF_SQL
279
+
280
+ if (@db_info || {}).fetch('MSSQL 2005 compatible', false).eql?(true)
281
+ parts = [intro]
282
+ (0...migrations.length).each do |i|
283
+ m = migrations[i]
284
+ description_literal = MSSQLSpecifics.string_literal(m.description.strip)
285
+ parts << mig_insert + "(N'#{m.id}', #{i + 1}, #{description_literal});\n"
286
+ end
287
+ return parts.join('')
288
+ else
289
+ return intro + mig_insert + (0...migrations.length).collect do |i|
290
+ m = migrations[i]
291
+ description_literal = MSSQLSpecifics.string_literal(m.description.strip)
292
+ "(N'#{m.id}', #{i + 1}, #{description_literal})"
293
+ end.join(",\n") + ";\n"
294
+ end
295
+ end
296
+
297
+ def create_and_fill_indexes_table_sql
298
+ intro = <<-"END_OF_SQL"
299
+ PRINT N'Creating and filling index manipulation table:';
300
+ IF EXISTS (
301
+ SELECT * FROM sys.objects
302
+ WHERE object_id = OBJECT_ID(N'[xmigra].[updated_indexes]')
303
+ AND type IN (N'U')
304
+ )
305
+ BEGIN
306
+ DROP TABLE [xmigra].[updated_indexes];
307
+ END;
308
+ GO
309
+
310
+ CREATE TABLE [xmigra].[updated_indexes] (
311
+ [IndexID] NVARCHAR(80) NOT NULL PRIMARY KEY
312
+ );
313
+ GO
314
+
315
+ END_OF_SQL
316
+
317
+ insertion = <<-"END_OF_SQL"
318
+ INSERT INTO [xmigra].[updated_indexes] ([IndexID]) VALUES
319
+ END_OF_SQL
320
+
321
+ strlit = MSSQLSpecifics.method :string_literal
322
+ return intro + (insertion + indexes.collect do |index|
323
+ "(#{strlit[index.id]})"
324
+ end.join(",\n") + ";\n" unless indexes.empty?).to_s
325
+
326
+ return intro
327
+ end
328
+
329
+ def create_and_fill_statistics_table_sql
330
+ intro = <<-"END_OF_SQL"
331
+ PRINT N'Creating and filling statistics object manipulation table:';
332
+ IF EXISTS (
333
+ SELECT * FROM sys.objects
334
+ WHERE object_id = OBJECT_ID(N'[xmigra].[updated_statistics]')
335
+ AND type in (N'U')
336
+ )
337
+ BEGIN
338
+ DROP TABLE [xmigra].[updated_statistics];
339
+ END;
340
+ GO
341
+
342
+ CREATE TABLE [xmigra].[updated_statistics] (
343
+ [Name] nvarchar(100) NOT NULL PRIMARY KEY,
344
+ [Columns] nvarchar(256) NOT NULL
345
+ );
346
+ GO
347
+
348
+ END_OF_SQL
349
+
350
+ insertion = <<-"END_OF_SQL"
351
+ INSERT INTO [xmigra].[updated_statistics] ([Name], [Columns]) VALUES
352
+ END_OF_SQL
353
+
354
+ strlit = MSSQLSpecifics.method :string_literal
355
+ return intro + (insertion + stats_objs.collect do |stats_obj|
356
+ "(#{strlit[stats_obj.name]}, #{strlit[stats_obj.columns]})"
357
+ end.join(",\n") + ";\n" unless stats_objs.empty?).to_s
358
+ end
359
+
360
+ def check_preceding_migrations_sql
361
+ parts = []
362
+
363
+ parts << (<<-"END_OF_SQL") if production
364
+ IF EXISTS (
365
+ SELECT TOP(1) * FROM [xmigra].[branch_upgrade]
366
+ ) AND NOT EXISTS (
367
+ SELECT TOP(1) * FROM [xmigra].[branch_upgrade]
368
+ WHERE #{branch_id_literal} IN ([Current], [Next])
369
+ )
370
+ RAISERROR (N'Existing database is from a different (and non-upgradable) branch.', 11, 1);
371
+
372
+ END_OF_SQL
373
+
374
+ parts << (<<-"END_OF_SQL")
375
+ IF NOT #{upgrading_to_new_branch_test_sql}
376
+ BEGIN
377
+ PRINT N'Checking preceding migrations:';
378
+ -- Get the ApplicationOrder of the most recent version bridge migration
379
+ DECLARE @VersionBridge INT;
380
+ SET @VersionBridge = (
381
+ SELECT COALESCE(MAX([ApplicationOrder]), 0)
382
+ FROM [xmigra].[applied]
383
+ WHERE [VersionBridgeMark] <> 0
384
+ );
385
+
386
+ -- Check for existence of applied migrations after the latest version
387
+ -- bridge that are not in [xmigra].[migrations]
388
+ IF EXISTS (
389
+ SELECT * FROM [xmigra].[applied] a
390
+ WHERE a.[ApplicationOrder] > @VersionBridge
391
+ AND a.[MigrationID] NOT IN (
392
+ SELECT m.[MigrationID] FROM [xmigra].[migrations] m
393
+ )
394
+ )
395
+ RAISERROR (N'Unknown in-version migrations have been applied.', 11, 1);
396
+ END;
397
+ END_OF_SQL
398
+
399
+ return parts.join('')
400
+ end
401
+
402
+ def check_chain_continuity_sql
403
+ <<-"END_OF_SQL"
404
+ IF NOT #{upgrading_to_new_branch_test_sql}
405
+ BEGIN
406
+ PRINT N'Checking migration chain continuity:';
407
+ -- Get the [xmigra].[migrations] ApplicationOrder of the most recent version bridge migration
408
+ DECLARE @BridgePoint INT;
409
+ SET @BridgePoint = (
410
+ SELECT COALESCE(MAX(m.[ApplicationOrder]), 0)
411
+ FROM [xmigra].[applied] a
412
+ INNER JOIN [xmigra].[migrations] m
413
+ ON a.[MigrationID] = m.[MigrationID]
414
+ WHERE a.[VersionBridgeMark] <> 0
415
+ );
416
+
417
+ -- Test for previously applied migrations that break the continuity of the
418
+ -- migration chain in this script:
419
+ IF EXISTS (
420
+ SELECT *
421
+ FROM [xmigra].[applied] a
422
+ INNER JOIN [xmigra].[migrations] m
423
+ ON a.[MigrationID] = m.[MigrationID]
424
+ INNER JOIN [xmigra].[migrations] p
425
+ ON m.[ApplicationOrder] - 1 = p.[ApplicationOrder]
426
+ WHERE p.[ApplicationOrder] > @BridgePoint
427
+ AND p.[MigrationID] NOT IN (
428
+ SELECT a2.[MigrationID] FROM [xmigra].[applied] a2
429
+ )
430
+ )
431
+ BEGIN
432
+ RAISERROR(
433
+ N'Previously applied migrations interrupt the continuity of the migration chain',
434
+ 11,
435
+ 1
436
+ );
437
+ END;
438
+ END;
439
+ END_OF_SQL
440
+ end
441
+
442
+ def select_for_install_sql
443
+ <<-"END_OF_SQL"
444
+ PRINT N'Selecting migrations to apply:';
445
+ DECLARE @BridgePoint INT;
446
+ IF #{upgrading_to_new_branch_test_sql}
447
+ BEGIN
448
+ -- Get the [xmigra].[migrations] ApplicationOrder of the record corresponding to the branch transition
449
+ SET @BridgePoint = (
450
+ SELECT MAX(m.[ApplicationOrder])
451
+ FROM [xmigra].[migrations] m
452
+ INNER JOIN [xmigra].[branch_upgrade] bu
453
+ ON m.[MigrationID] = bu.[CompletesMigration]
454
+ );
455
+
456
+ UPDATE [xmigra].[migrations]
457
+ SET [Install] = 1
458
+ WHERE [ApplicationOrder] > @BridgePoint;
459
+ END
460
+ ELSE BEGIN
461
+ -- Get the [xmigra].[migrations] ApplicationOrder of the most recent version bridge migration
462
+ SET @BridgePoint = (
463
+ SELECT COALESCE(MAX(m.[ApplicationOrder]), 0)
464
+ FROM [xmigra].[applied] a
465
+ INNER JOIN [xmigra].[migrations] m
466
+ ON a.[MigrationID] = m.[MigrationID]
467
+ WHERE a.[VersionBridgeMark] <> 0
468
+ );
469
+
470
+ UPDATE [xmigra].[migrations]
471
+ SET [Install] = 1
472
+ WHERE [MigrationID] NOT IN (
473
+ SELECT a.[MigrationID] FROM [xmigra].[applied] a
474
+ )
475
+ AND [ApplicationOrder] > @BridgePoint;
476
+ END;
477
+ END_OF_SQL
478
+ end
479
+
480
+ def production_config_check_sql
481
+ unless production
482
+ id_literal = MSSQLSpecifics.string_literal(@migrations[0].id)
483
+ <<-"END_OF_SQL"
484
+ PRINT N'Checking for production status:';
485
+ IF EXISTS (
486
+ SELECT * FROM [xmigra].[migrations]
487
+ WHERE [MigrationID] = #{id_literal}
488
+ AND [Install] <> 0
489
+ )
490
+ BEGIN
491
+ CREATE TABLE [xmigra].[development] (
492
+ [info] nvarchar(200) NOT NULL PRIMARY KEY
493
+ );
494
+ END;
495
+ GO
496
+
497
+ IF NOT EXISTS (
498
+ SELECT * FROM [sys].[objects]
499
+ WHERE object_id = OBJECT_ID(N'[xmigra].[development]')
500
+ AND type = N'U'
501
+ )
502
+ RAISERROR(N'Development script cannot be applied to a production database.', 11, 1);
503
+ END_OF_SQL
504
+ end
505
+ end
506
+
507
+ def remove_access_artifacts_sql
508
+ # Iterate the [xmigra].[access_objects] table and drop all access
509
+ # objects previously created by xmigra
510
+ return <<-"END_OF_SQL"
511
+ PRINT N'Removing data access artifacts:';
512
+ DECLARE @sqlcmd NVARCHAR(1000); -- Built SQL command
513
+ DECLARE @obj_name NVARCHAR(256); -- Name of object to drop
514
+ DECLARE @obj_type NVARCHAR(40); -- Type of object to drop
515
+
516
+ DECLARE AccObjs_cursor CURSOR LOCAL FOR
517
+ SELECT [name], [type]
518
+ FROM [xmigra].[access_objects]
519
+ ORDER BY [order] DESC;
520
+
521
+ OPEN AccObjs_cursor;
522
+
523
+ FETCH NEXT FROM AccObjs_cursor INTO @obj_name, @obj_type;
524
+
525
+ WHILE @@FETCH_STATUS = 0 BEGIN
526
+ SET @sqlcmd = N'DROP ' + @obj_type + N' ' + @obj_name + N';';
527
+ EXEC sp_executesql @sqlcmd;
528
+
529
+ FETCH NEXT FROM AccObjs_cursor INTO @obj_name, @obj_type;
530
+ END;
531
+
532
+ CLOSE AccObjs_cursor;
533
+ DEALLOCATE AccObjs_cursor;
534
+
535
+ DELETE FROM [xmigra].[access_objects];
536
+
537
+ END_OF_SQL
538
+ end
539
+
540
+ def remove_undesired_indexes_sql
541
+ <<-"END_OF_SQL"
542
+ PRINT N'Removing undesired indexes:';
543
+ -- Iterate over indexes in [xmigra].[indexes] that don't have an entry in
544
+ -- [xmigra].[updated_indexes].
545
+ DECLARE @sqlcmd NVARCHAR(1000); -- Built SQL command
546
+ DECLARE @index_name NVARCHAR(256); -- Name of index to drop
547
+ DECLARE @table_name SYSNAME; -- Name of table owning index
548
+ DECLARE @match_count INT; -- Number of matching index names
549
+
550
+ DECLARE Index_cursor CURSOR LOCAL FOR
551
+ SELECT
552
+ xi.[name],
553
+ MAX(QUOTENAME(OBJECT_SCHEMA_NAME(si.object_id)) + N'.' + QUOTENAME(OBJECT_NAME(si.object_id))),
554
+ COUNT(*)
555
+ FROM [xmigra].[indexes] xi
556
+ INNER JOIN sys.indexes si ON si.[name] = xi.[name]
557
+ WHERE xi.[IndexID] NOT IN (
558
+ SELECT [IndexID]
559
+ FROM [xmigra].[updated_indexes]
560
+ )
561
+ GROUP BY xi.[name];
562
+
563
+ OPEN Index_cursor;
564
+
565
+ FETCH NEXT FROM Index_cursor INTO @index_name, @table_name, @match_count;
566
+
567
+ WHILE @@FETCH_STATUS = 0 BEGIN
568
+ IF @match_count > 1
569
+ BEGIN
570
+ RAISERROR(N'Multiple indexes are named %s', 11, 1, @index_name);
571
+ END;
572
+
573
+ SET @sqlcmd = N'DROP INDEX ' + @index_name + N' ON ' + @table_name + N';';
574
+ EXEC sp_executesql @sqlcmd;
575
+ PRINT N' Removed ' + @index_name + N'.';
576
+
577
+ FETCH NEXT FROM Index_cursor INTO @index_name, @table_name, @match_count;
578
+ END;
579
+
580
+ CLOSE Index_cursor;
581
+ DEALLOCATE Index_cursor;
582
+
583
+ DELETE FROM [xmigra].[indexes]
584
+ WHERE [IndexID] NOT IN (
585
+ SELECT ui.[IndexID]
586
+ FROM [xmigra].[updated_indexes] ui
587
+ );
588
+ END_OF_SQL
589
+ end
590
+
591
+ def remove_undesired_statistics_sql
592
+ <<-"END_OF_SQL"
593
+ PRINT N'Removing undesired statistics objects:';
594
+ -- Iterate over statistics in [xmigra].[statistics] that don't have an entry in
595
+ -- [xmigra].[updated_statistics].
596
+ DECLARE @sqlcmd NVARCHAR(1000); -- Built SQL command
597
+ DECLARE @statsobj_name NVARCHAR(256); -- Name of statistics object to drop
598
+ DECLARE @table_name SYSNAME; -- Name of table owning the statistics object
599
+ DECLARE @match_count INT; -- Number of matching statistics object names
600
+
601
+ DECLARE Stats_cursor CURSOR LOCAL FOR
602
+ SELECT
603
+ QUOTENAME(xs.[Name]),
604
+ MAX(QUOTENAME(OBJECT_SCHEMA_NAME(ss.object_id)) + N'.' + QUOTENAME(OBJECT_NAME(ss.object_id))),
605
+ COUNT(ss.object_id)
606
+ FROM [xmigra].[statistics] xs
607
+ INNER JOIN sys.stats ss ON ss.[name] = xs.[Name]
608
+ WHERE xs.[Columns] NOT IN (
609
+ SELECT us.[Columns]
610
+ FROM [xmigra].[updated_statistics] us
611
+ WHERE us.[Name] = xs.[Name]
612
+ )
613
+ GROUP BY xs.[Name];
614
+
615
+ OPEN Stats_cursor;
616
+
617
+ FETCH NEXT FROM Stats_cursor INTO @statsobj_name, @table_name, @match_count;
618
+
619
+ WHILE @@FETCH_STATUS = 0 BEGIN
620
+ IF @match_count > 1
621
+ BEGIN
622
+ RAISERROR(N'Multiple indexes are named %s', 11, 1, @statsobj_name);
623
+ END;
624
+
625
+ SET @sqlcmd = N'DROP STATISTICS ' + @table_name + N'.' + @statsobj_name + N';';
626
+ EXEC sp_executesql @sqlcmd;
627
+ PRINT N' Removed statistics object ' + @statsobj_name + N'.'
628
+
629
+ FETCH NEXT FROM Stats_cursor INTO @statsobj_name, @table_name, @match_count;
630
+ END;
631
+
632
+ CLOSE Stats_cursor;
633
+ DEALLOCATE Stats_cursor;
634
+
635
+ DELETE FROM [xmigra].[statistics]
636
+ WHERE [Columns] NOT IN (
637
+ SELECT us.[Columns]
638
+ FROM [xmigra].[updated_statistics] us
639
+ WHERE us.[Name] = [Name]
640
+ );
641
+ END_OF_SQL
642
+ end
643
+
644
+ def create_new_indexes_sql
645
+ indexes.collect do |index|
646
+ index_id_literal = MSSQLSpecifics.string_literal(index.id)
647
+ index_name_literal = MSSQLSpecifics.string_literal(index.name)
648
+ <<-"END_OF_SQL"
649
+ PRINT N'Index ' + #{index_id_literal} + ':';
650
+ IF EXISTS(
651
+ SELECT * FROM [xmigra].[updated_indexes] ui
652
+ WHERE ui.[IndexID] = #{index_id_literal}
653
+ AND ui.[IndexID] NOT IN (
654
+ SELECT i.[IndexID] FROM [xmigra].[indexes] i
655
+ )
656
+ )
657
+ BEGIN
658
+ IF EXISTS (
659
+ SELECT * FROM sys.indexes
660
+ WHERE [name] = #{index_name_literal}
661
+ )
662
+ BEGIN
663
+ RAISERROR(N'An index already exists named %s', 11, 1, #{index_name_literal});
664
+ END;
665
+
666
+ PRINT N' Creating...';
667
+ #{index.definition_sql};
668
+
669
+ IF (SELECT COUNT(*) FROM sys.indexes WHERE [name] = #{index_name_literal}) <> 1
670
+ BEGIN
671
+ RAISERROR(N'Index %s was not created by its definition.', 11, 1,
672
+ #{index_name_literal});
673
+ END;
674
+
675
+ INSERT INTO [xmigra].[indexes] ([IndexID], [name])
676
+ VALUES (#{index_id_literal}, #{index_name_literal});
677
+ END
678
+ ELSE
679
+ BEGIN
680
+ PRINT N' Already exists.';
681
+ END;
682
+ END_OF_SQL
683
+ end.join(ddl_block_separator)
684
+ end
685
+
686
+ def create_new_statistics_sql
687
+ stats_objs.collect do |stats_obj|
688
+ stats_name = MSSQLSpecifics.string_literal(stats_obj.name)
689
+ strlit = lambda {|s| MSSQLSpecifics.string_literal(s)}
690
+
691
+ stats_obj.creation_sql
692
+ <<-"END_OF_SQL"
693
+ PRINT N'Statistics object #{stats_obj.name}:';
694
+ IF EXISTS (
695
+ SELECT * FROM [xmigra].[updated_statistics] us
696
+ WHERE us.[Name] = #{stats_name}
697
+ AND us.[Columns] NOT IN (
698
+ SELECT s.[Columns]
699
+ FROM [xmigra].[statistics] s
700
+ WHERE s.[Name] = us.[Name]
701
+ )
702
+ )
703
+ BEGIN
704
+ IF EXISTS (
705
+ SELECT * FROM sys.stats
706
+ WHERE [name] = #{stats_name}
707
+ )
708
+ BEGIN
709
+ RAISERROR(N'A statistics object named %s already exists.', 11, 1, #{stats_name})
710
+ END;
711
+
712
+ PRINT N' Creating...';
713
+ #{stats_obj.creation_sql}
714
+
715
+ INSERT INTO [xmigra].[statistics] ([Name], [Columns])
716
+ VALUES (#{stats_name}, #{strlit[stats_obj.columns]})
717
+ END
718
+ ELSE
719
+ BEGIN
720
+ PRINT N' Already exists.';
721
+ END;
722
+ END_OF_SQL
723
+ end.join(ddl_block_separator)
724
+ end
725
+
726
+ def upgrade_cleanup_sql
727
+ <<-"END_OF_SQL"
728
+ PRINT N'Cleaning up from the upgrade:';
729
+ DROP TABLE [xmigra].[migrations];
730
+ DROP TABLE [xmigra].[updated_indexes];
731
+ DROP TABLE [xmigra].[updated_statistics];
732
+ END_OF_SQL
733
+ end
734
+
735
+ def ensure_permissions_table_sql
736
+ strlit = MSSQLSpecifics.method(:string_literal)
737
+ <<-"END_OF_SQL"
738
+ -- ------------ SET UP XMIGRA PERMISSION TRACKING OBJECTS ------------ --
739
+
740
+ PRINT N'Setting up XMigra permission tracking:';
741
+ IF NOT EXISTS (
742
+ SELECT * FROM sys.schemas
743
+ WHERE name = N'xmigra'
744
+ )
745
+ BEGIN
746
+ EXEC sp_executesql N'
747
+ CREATE SCHEMA [xmigra] AUTHORIZATION [dbo];
748
+ ';
749
+ END;
750
+ GO
751
+
752
+ IF NOT EXISTS(
753
+ SELECT * FROM sys.objects
754
+ WHERE object_id = OBJECT_ID(N'[xmigra].[revokable_permissions]')
755
+ AND type IN (N'U')
756
+ )
757
+ BEGIN
758
+ CREATE TABLE [xmigra].[revokable_permissions] (
759
+ [permissions] nvarchar(200) NOT NULL,
760
+ [object] nvarchar(260) NOT NULL,
761
+ [principal_id] int NOT NULL
762
+ ) ON [PRIMARY];
763
+ END;
764
+ GO
765
+
766
+ IF EXISTS(
767
+ SELECT * FROM sys.objects
768
+ WHERE object_id = OBJECT_ID(N'[xmigra].[ip_prepare_revoke]')
769
+ AND type IN (N'P', N'PC')
770
+ )
771
+ BEGIN
772
+ DROP PROCEDURE [xmigra].[ip_prepare_revoke];
773
+ END;
774
+ GO
775
+
776
+ CREATE PROCEDURE [xmigra].[ip_prepare_revoke]
777
+ (
778
+ @permissions nvarchar(200),
779
+ @object nvarchar(260),
780
+ @principal sysname
781
+ )
782
+ AS
783
+ BEGIN
784
+ INSERT INTO [xmigra].[revokable_permissions] ([permissions], [object], [principal_id])
785
+ VALUES (@permissions, @object, DATABASE_PRINCIPAL_ID(@principal));
786
+ END;
787
+ END_OF_SQL
788
+ end
789
+
790
+ def revoke_previous_permissions_sql
791
+ <<-"END_OF_SQL"
792
+
793
+ -- ------------- REVOKING PREVIOUSLY GRANTED PERMISSIONS ------------- --
794
+
795
+ PRINT N'Revoking previously granted permissions:';
796
+ -- Iterate over permissions listed in [xmigra].[revokable_permissions]
797
+ DECLARE @sqlcmd NVARCHAR(1000); -- Built SQL command
798
+ DECLARE @permissions NVARCHAR(200);
799
+ DECLARE @object NVARCHAR(260);
800
+ DECLARE @principal NVARCHAR(150);
801
+
802
+ DECLARE Permission_cursor CURSOR LOCAL FOR
803
+ SELECT
804
+ xp.[permissions],
805
+ xp.[object],
806
+ QUOTENAME(sdp.name)
807
+ FROM [xmigra].[revokable_permissions] xp
808
+ INNER JOIN sys.database_principals sdp ON xp.principal_id = sdp.principal_id;
809
+
810
+ OPEN Permission_cursor;
811
+
812
+ FETCH NEXT FROM Permission_cursor INTO @permissions, @object, @principal;
813
+
814
+ WHILE @@FETCH_STATUS = 0 BEGIN
815
+ SET @sqlcmd = N'REVOKE ' + @permissions + N' ON ' + @object + ' FROM ' + @principal + N';';
816
+ BEGIN TRY
817
+ EXEC sp_executesql @sqlcmd;
818
+ END TRY
819
+ BEGIN CATCH
820
+ END CATCH
821
+
822
+ FETCH NEXT FROM Permission_cursor INTO @permissions, @object, @principal;
823
+ END;
824
+
825
+ CLOSE Permission_cursor;
826
+ DEALLOCATE Permission_cursor;
827
+
828
+ DELETE FROM [xmigra].[revokable_permissions];
829
+ END_OF_SQL
830
+ end
831
+
832
+ def granting_permissions_comment_sql
833
+ <<-"END_OF_SQL"
834
+
835
+ -- ---------------------- GRANTING PERMISSIONS ----------------------- --
836
+
837
+ END_OF_SQL
838
+ end
839
+
840
+ def grant_permissions_sql(permissions, object, principal)
841
+ strlit = MSSQLSpecifics.method(:string_literal)
842
+ permissions_string = permissions.to_a.join(', ')
843
+
844
+ <<-"END_OF_SQL"
845
+ PRINT N'Granting #{permissions_string} on #{object} to #{principal}:';
846
+ GRANT #{permissions_string} ON #{object} TO #{principal};
847
+ EXEC [xmigra].[ip_prepare_revoke] #{strlit[permissions_string]}, #{strlit[object]}, #{strlit[principal]};
848
+ END_OF_SQL
849
+ end
850
+
851
+ def insert_access_creation_record_sql
852
+ name_literal = MSSQLSpecifics.string_literal(quoted_name)
853
+
854
+ <<-"END_OF_SQL"
855
+ INSERT INTO [xmigra].[access_objects] ([type], [name])
856
+ VALUES (N'#{self.class::OBJECT_TYPE}', #{name_literal});
857
+ END_OF_SQL
858
+ end
859
+
860
+ # Call on an extended Migration object to get the SQL to execute.
861
+ def migration_application_sql
862
+ id_literal = MSSQLSpecifics.string_literal(id)
863
+ template = <<-"END_OF_SQL"
864
+ IF EXISTS (
865
+ SELECT * FROM [xmigra].[migrations]
866
+ WHERE [MigrationID] = #{id_literal}
867
+ AND [Install] <> 0
868
+ )
869
+ BEGIN
870
+ PRINT #{MSSQLSpecifics.string_literal('Applying "' + File.basename(file_path) + '":')};
871
+
872
+ %s
873
+
874
+ INSERT INTO [xmigra].[applied] ([MigrationID], [Description])
875
+ VALUES (#{id_literal}, #{MSSQLSpecifics.string_literal(description)});
876
+ END;
877
+ END_OF_SQL
878
+
879
+ parts = []
880
+
881
+ each_batch(sql) do |batch|
882
+ parts << batch
883
+ end
884
+
885
+ return (template % parts.collect do |batch|
886
+ "EXEC sp_executesql @statement = " + MSSQLSpecifics.string_literal(batch) + ";"
887
+ end.join("\n"))
888
+ end
889
+
890
+ def each_batch(sql)
891
+ current_batch_lines = []
892
+ sql.each_line do |line|
893
+ if line.strip.upcase == 'GO'
894
+ batch = current_batch_lines.join('')
895
+ yield batch unless batch.strip.empty?
896
+ current_batch_lines.clear
897
+ else
898
+ current_batch_lines << line
899
+ end
900
+ end
901
+ unless current_batch_lines.empty?
902
+ batch = current_batch_lines.join('')
903
+ yield batch unless batch.strip.empty?
904
+ end
905
+ end
906
+
907
+ def batch_separator
908
+ "GO\n"
909
+ end
910
+
911
+ def check_existence_sql(for_existence, error_message)
912
+ error_message = sprintf(error_message, quoted_name)
913
+
914
+ return <<-"END_OF_SQL"
915
+
916
+ IF #{"NOT" if for_existence} #{existence_test_sql}
917
+ RAISERROR(N'#{error_message}', 11, 1);
918
+ END_OF_SQL
919
+ end
920
+
921
+ def creation_notice
922
+ return "PRINT " + MSSQLSpecifics.string_literal("Creating #{printable_type} #{quoted_name}:") + ";"
923
+ end
924
+
925
+ def name_parts
926
+ if m = DBNAME_PATTERN.match(name)
927
+ [m[1], m[2]].compact.collect do |p|
928
+ MSSQLSpecifics.strip_identifier_quoting(p)
929
+ end
930
+ else
931
+ raise XMigra::Error, "Invalid database object name"
932
+ end
933
+ end
934
+
935
+ def quoted_name
936
+ name_parts.collect do |p|
937
+ "[]".insert(1, p)
938
+ end.join('.')
939
+ end
940
+
941
+ def object_type_codes
942
+ MSSQLSpecifics.object_type_codes(self)
943
+ end
944
+
945
+ def existence_test_sql
946
+ object_type_list = object_type_codes.collect {|t| "N'#{t}'"}.join(', ')
947
+
948
+ return <<-"END_OF_SQL"
949
+ EXISTS (
950
+ SELECT * FROM sys.objects
951
+ WHERE object_id = OBJECT_ID(N'#{quoted_name}')
952
+ AND type IN (#{object_type_list})
953
+ )
954
+ END_OF_SQL
955
+ end
956
+
957
+ def branch_id_literal
958
+ @mssql_branch_id_literal ||= MSSQLSpecifics.string_literal(XMigra.secure_digest(branch_identifier))
959
+ end
960
+
961
+ def upgrading_to_new_branch_test_sql
962
+ return "(0 = 1)" unless respond_to? :branch_identifier
963
+
964
+ (<<-"END_OF_SQL").chomp
965
+ (EXISTS (
966
+ SELECT TOP(1) * FROM [xmigra].[branch_upgrade]
967
+ WHERE [Next] = #{branch_id_literal}
968
+ ))
969
+ END_OF_SQL
970
+ end
971
+
972
+ def branch_upgrade_sql
973
+ return unless respond_to? :branch_identifier
974
+
975
+ parts = [<<-"END_OF_SQL"]
976
+ IF #{upgrading_to_new_branch_test_sql}
977
+ BEGIN
978
+ PRINT N'Migrating from previous schema branch:';
979
+
980
+ DECLARE @sqlcmd NVARCHAR(MAX);
981
+
982
+ DECLARE CmdCursor CURSOR LOCAL FOR
983
+ SELECT bu.[UpgradeSql]
984
+ FROM [xmigra].[branch_upgrade] bu
985
+ WHERE bu.[Next] = #{branch_id_literal}
986
+ ORDER BY bu.[ApplicationOrder] ASC;
987
+
988
+ OPEN CmdCursor;
989
+
990
+ FETCH NEXT FROM CmdCursor INTO @sqlcmd;
991
+
992
+ WHILE @@FETCH_STATUS = 0 BEGIN
993
+ EXECUTE sp_executesql @sqlcmd;
994
+
995
+ FETCH NEXT FROM CmdCursor INTO @sqlcmd;
996
+ END;
997
+
998
+ CLOSE CmdCursor;
999
+ DEALLOCATE CmdCursor;
1000
+
1001
+ DECLARE @applied NVARCHAR(80);
1002
+ DECLARE @old_branch NVARCHAR(80);
1003
+
1004
+ SELECT TOP(1) @applied = [CompletesMigration], @old_branch = [Current]
1005
+ FROM [xmigra].[branch_upgrade]
1006
+ WHERE [Next] = #{branch_id_literal};
1007
+
1008
+ -- Delete the "applied" record for the migration if there was one, so that
1009
+ -- a new record with this ID can be inserted.
1010
+ DELETE FROM [xmigra].[applied] WHERE [MigrationID] = @applied;
1011
+
1012
+ -- Create a "version bridge" record in the "applied" table for the branch upgrade
1013
+ INSERT INTO [xmigra].[applied] ([MigrationID], [VersionBridgeMark], [Description])
1014
+ VALUES (@applied, 1, N'Branch upgrade from branch ' + @old_branch);
1015
+ END;
1016
+
1017
+ DELETE FROM [xmigra].[branch_upgrade];
1018
+
1019
+ END_OF_SQL
1020
+
1021
+ if branch_upgrade.applicable? migrations
1022
+ batch_template = <<-"END_OF_SQL"
1023
+ INSERT INTO [xmigra].[branch_upgrade]
1024
+ ([Current], [Next], [CompletesMigration], [UpgradeSql])
1025
+ VALUES (
1026
+ #{branch_id_literal},
1027
+ #{MSSQLSpecifics.string_literal(branch_upgrade.target_branch)},
1028
+ #{MSSQLSpecifics.string_literal(branch_upgrade.migration_completed_id)},
1029
+ %s
1030
+ );
1031
+ END_OF_SQL
1032
+
1033
+ each_batch(branch_upgrade.sql) do |batch|
1034
+ # Insert the batch into the [xmigra].[branch_upgrade] table
1035
+ parts << (batch_template % MSSQLSpecifics.string_literal(batch))
1036
+ end
1037
+ else
1038
+ # Insert a placeholder that only declares the current branch of the schema
1039
+ parts << <<-"END_OF_SQL"
1040
+ INSERT INTO [xmigra].[branch_upgrade] ([Current]) VALUES (#{branch_id_literal});
1041
+ END_OF_SQL
1042
+ end
1043
+
1044
+ return parts.join("\n")
1045
+ end
1046
+
1047
+ class << self
1048
+ def strip_identifier_quoting(s)
1049
+ case
1050
+ when s.empty? then return s
1051
+ when s[0,1] == "[" && s[-1,1] == "]" then return s[1..-2]
1052
+ when s[0,1] == '"' && s[-1,1] == '"' then return s[1..-2]
1053
+ else return s
1054
+ end
1055
+ end
1056
+
1057
+ def object_type_codes(type)
1058
+ case type
1059
+ when StoredProcedure then %w{P PC}
1060
+ when View then ['V']
1061
+ when Function then %w{AF FN FS FT IF TF}
1062
+ end
1063
+ end
1064
+
1065
+ def string_literal(s)
1066
+ "N'#{s.gsub("'","''")}'"
1067
+ end
1068
+ end
1069
+ end
1070
+ end