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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +53 -0
- data/Rakefile +1 -0
- data/bin/mkxms-mssql +5 -0
- data/lib/mkxms/mssql/access_object_definition.rb +61 -0
- data/lib/mkxms/mssql/adoption_script_writer.rb +1486 -0
- data/lib/mkxms/mssql/check_constraint_handler.rb +56 -0
- data/lib/mkxms/mssql/database_handler.rb +339 -0
- data/lib/mkxms/mssql/default_constraint_handler.rb +46 -0
- data/lib/mkxms/mssql/engine.rb +88 -0
- data/lib/mkxms/mssql/exceptions.rb +10 -0
- data/lib/mkxms/mssql/filegroup_handler.rb +81 -0
- data/lib/mkxms/mssql/foreign_key_handler.rb +85 -0
- data/lib/mkxms/mssql/function_handler.rb +74 -0
- data/lib/mkxms/mssql/indented_string_builder.rb +199 -0
- data/lib/mkxms/mssql/index_column.rb +11 -0
- data/lib/mkxms/mssql/index_handler.rb +98 -0
- data/lib/mkxms/mssql/keylike_constraint_helper.rb +67 -0
- data/lib/mkxms/mssql/permission_handler.rb +115 -0
- data/lib/mkxms/mssql/primary_key_handler.rb +36 -0
- data/lib/mkxms/mssql/property_handler.rb +87 -0
- data/lib/mkxms/mssql/query_cursor.rb +111 -0
- data/lib/mkxms/mssql/role_handler.rb +55 -0
- data/lib/mkxms/mssql/schema_handler.rb +42 -0
- data/lib/mkxms/mssql/sql_string_manipulators.rb +46 -0
- data/lib/mkxms/mssql/statistics_handler.rb +59 -0
- data/lib/mkxms/mssql/stored_procedure_handler.rb +65 -0
- data/lib/mkxms/mssql/table_handler.rb +180 -0
- data/lib/mkxms/mssql/unique_constraint_handler.rb +32 -0
- data/lib/mkxms/mssql/utils.rb +83 -0
- data/lib/mkxms/mssql/version.rb +5 -0
- data/lib/mkxms/mssql/view_handler.rb +58 -0
- data/lib/mkxms/mssql.rb +62 -0
- data/mkxms-mssql.gemspec +26 -0
- data/spec/utils/indented_string_builder_spec.rb +218 -0
- data/spec/utils/query_cursor_spec.rb +57 -0
- 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
data/Gemfile
ADDED
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,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
|