xmigra 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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