mkxms-mssql 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +53 -0
  6. data/Rakefile +1 -0
  7. data/bin/mkxms-mssql +5 -0
  8. data/lib/mkxms/mssql/access_object_definition.rb +61 -0
  9. data/lib/mkxms/mssql/adoption_script_writer.rb +1486 -0
  10. data/lib/mkxms/mssql/check_constraint_handler.rb +56 -0
  11. data/lib/mkxms/mssql/database_handler.rb +339 -0
  12. data/lib/mkxms/mssql/default_constraint_handler.rb +46 -0
  13. data/lib/mkxms/mssql/engine.rb +88 -0
  14. data/lib/mkxms/mssql/exceptions.rb +10 -0
  15. data/lib/mkxms/mssql/filegroup_handler.rb +81 -0
  16. data/lib/mkxms/mssql/foreign_key_handler.rb +85 -0
  17. data/lib/mkxms/mssql/function_handler.rb +74 -0
  18. data/lib/mkxms/mssql/indented_string_builder.rb +199 -0
  19. data/lib/mkxms/mssql/index_column.rb +11 -0
  20. data/lib/mkxms/mssql/index_handler.rb +98 -0
  21. data/lib/mkxms/mssql/keylike_constraint_helper.rb +67 -0
  22. data/lib/mkxms/mssql/permission_handler.rb +115 -0
  23. data/lib/mkxms/mssql/primary_key_handler.rb +36 -0
  24. data/lib/mkxms/mssql/property_handler.rb +87 -0
  25. data/lib/mkxms/mssql/query_cursor.rb +111 -0
  26. data/lib/mkxms/mssql/role_handler.rb +55 -0
  27. data/lib/mkxms/mssql/schema_handler.rb +42 -0
  28. data/lib/mkxms/mssql/sql_string_manipulators.rb +46 -0
  29. data/lib/mkxms/mssql/statistics_handler.rb +59 -0
  30. data/lib/mkxms/mssql/stored_procedure_handler.rb +65 -0
  31. data/lib/mkxms/mssql/table_handler.rb +180 -0
  32. data/lib/mkxms/mssql/unique_constraint_handler.rb +32 -0
  33. data/lib/mkxms/mssql/utils.rb +83 -0
  34. data/lib/mkxms/mssql/version.rb +5 -0
  35. data/lib/mkxms/mssql/view_handler.rb +58 -0
  36. data/lib/mkxms/mssql.rb +62 -0
  37. data/mkxms-mssql.gemspec +26 -0
  38. data/spec/utils/indented_string_builder_spec.rb +218 -0
  39. data/spec/utils/query_cursor_spec.rb +57 -0
  40. metadata +142 -0
@@ -0,0 +1,56 @@
1
+ require 'mkxms/mssql/property_handler'
2
+
3
+ module Mkxms; end
4
+
5
+ module Mkxms::Mssql
6
+ class CheckConstraint
7
+ include ExtendedProperties, Property::Hosting
8
+
9
+ def initialize(schema, table, name, enabled: true, when_replicated: true)
10
+ @schema = schema
11
+ @table = table
12
+ @name = name
13
+ @enabled = enabled
14
+ @when_replicated = when_replicated
15
+ @expression = ''
16
+ end
17
+
18
+ attr_accessor :schema, :table, :name, :enabled, :when_replicated
19
+ attr_reader :expression
20
+
21
+ def to_sql
22
+ "ALTER TABLE #@schema.#@table ADD%s CHECK%s #@expression;%s" % [
23
+ @name ? " CONSTRAINT #@name" : '',
24
+ @when_replicated ? '' : ' NOT FOR REPLICATION',
25
+ @enabled ? '' : "\nALTER TABLE #@schema.#@table NOCHECK CONSTRAINT #@name;"
26
+ ] + (name ? extended_properties_sql.joined_on_new_lines : '')
27
+ end
28
+
29
+ def qualified_table
30
+ "#@schema.#@table"
31
+ end
32
+
33
+ def qualified_name
34
+ "#@schema.#@name" if @name
35
+ end
36
+
37
+ def property_subject_identifiers
38
+ ['SCHEMA', schema, 'TABLE', table, 'CONSTRAINT', name].map {|s| Utils::unquoted_name(s)}
39
+ end
40
+ end
41
+
42
+ class CheckConstraintHandler
43
+ include PropertyHandler::ElementHandler
44
+
45
+ def initialize(constraints, node)
46
+ a = node.attributes
47
+
48
+ @check = CheckConstraint.new(a['schema'], a['table'], a['name'],
49
+ enabled: !a['disabled'], when_replicated: !a['not-for-replication'])
50
+ end
51
+
52
+ def handle_text(text, parent_element)
53
+ @check.expression << text
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,339 @@
1
+ require 'pathname'
2
+ require 'set'
3
+ require 'xmigra'
4
+ require 'yaml'
5
+
6
+ %w[
7
+
8
+ adoption_script_writer
9
+ check_constraint_handler
10
+ default_constraint_handler
11
+ filegroup_handler
12
+ foreign_key_handler
13
+ function_handler
14
+ index_handler
15
+ permission_handler
16
+ primary_key_handler
17
+ property_handler
18
+ role_handler
19
+ schema_handler
20
+ statistics_handler
21
+ stored_procedure_handler
22
+ table_handler
23
+ unique_constraint_handler
24
+ utils
25
+ view_handler
26
+
27
+ ].each {|f| require "mkxms/mssql/" + f}
28
+
29
+ module Mkxms; end
30
+
31
+ module Mkxms::Mssql
32
+ class Mkxms::Mssql::DatabaseHandler
33
+ extend Utils::InitializedAttributes
34
+ include ExtendedProperties, PropertyHandler::ElementHandler
35
+
36
+ ADOPTION_SQL_FILE = "adopt.sql"
37
+
38
+ def initialize(**kwargs)
39
+ @schema_dir = kwargs[:schema_dir] || Pathname.pwd
40
+ end
41
+
42
+ attr_reader :schema_dir
43
+ attr_init(:filegroups, :schemas, :roles, :tables, :column_defaults, :pku_constraints, :foreign_keys, :check_constraints){[]}
44
+ attr_init(:indexes, :statistics){[]}
45
+ attr_init(:views, :udfs, :procedures){[]}
46
+ attr_init(:permissions){[]}
47
+
48
+ def handle_database_element(parse)
49
+ end
50
+
51
+ def handle_filegroup_element(parse)
52
+ parse.delegate_to FilegroupHandler, filegroups
53
+ end
54
+
55
+ def handle_fulltext_document_type_element(parse)
56
+ # TODO: Check that these types are registered in the target instance
57
+ end
58
+
59
+ def handle_schema_element(parse)
60
+ parse.delegate_to SchemaHandler, schemas
61
+ end
62
+
63
+ def handle_role_element(parse)
64
+ parse.delegate_to RoleHandler, roles
65
+ end
66
+
67
+ def handle_table_element(parse)
68
+ parse.delegate_to TableHandler, tables
69
+ end
70
+
71
+ def handle_default_constraint_element(parse)
72
+ parse.delegate_to DefaultConstraintHandler, column_defaults
73
+ end
74
+
75
+ def handle_primary_key_element(parse)
76
+ parse.delegate_to PrimaryKeyHandler, pku_constraints
77
+ end
78
+
79
+ def handle_foreign_key_element(parse)
80
+ parse.delegate_to ForeignKeyHandler, foreign_keys
81
+ end
82
+
83
+ def handle_unique_constraint_element(parse)
84
+ parse.delegate_to UniqueConstraintHandler, pku_constraints
85
+ end
86
+
87
+ def handle_check_constraint_element(parse)
88
+ parse.delegate_to CheckConstraintHandler, check_constraints
89
+ end
90
+
91
+ def handle_index_element(parse)
92
+ parse.delegate_to IndexHandler, indexes
93
+ end
94
+
95
+ def handle_statistics_element(parse)
96
+ parse.delegate_to StatisticsHandler, statistics
97
+ end
98
+
99
+ def handle_view_element(parse)
100
+ parse.delegate_to ViewHandler, views
101
+ end
102
+
103
+ def handle_stored_procedure_element(parse)
104
+ parse.delegate_to StoredProcedureHandler, procedures
105
+ end
106
+
107
+ def handle_user_defined_function_element(parse)
108
+ parse.delegate_to FunctionHandler, udfs
109
+ end
110
+
111
+ def handle_granted_element(parse)
112
+ parse.delegate_to PermissionHandler, permissions
113
+ end
114
+
115
+ def handle_denied_element(parse)
116
+ parse.delegate_to PermissionHandler, permissions
117
+ end
118
+
119
+ def create_source_files
120
+ dbinfo_path = @schema_dir.join(XMigra::SchemaManipulator::DBINFO_FILE)
121
+
122
+ if dbinfo_path.exist?
123
+ raise ProgramArgumentError.new("#{@schema_dir} already contains an XMigra schema")
124
+ end
125
+
126
+ # TODO: Sort dependencies of triggers, views, user defined functions, and
127
+ # stored procedures to determine which ones must be incorporated into a
128
+ # migration (all the ones depended on by any triggers).
129
+
130
+ # Create schema_dir if it does not exist
131
+ @schema_dir.mkpath
132
+
133
+ # Create and populate @schema_dir + XMigra::SchemaManipulator::DBINFO_FILE
134
+ dbinfo_path.open('w') do |dbi|
135
+ dbi.puts "system: #{XMigra::MSSQLSpecifics::SYSTEM_NAME}"
136
+ end
137
+
138
+ # TODO: Create migration to check required filegroups and files
139
+
140
+ # Migration: Create roles
141
+ create_migration(
142
+ "create-roles",
143
+ "Create roles for accessing the database.",
144
+ (roles.map(&:definition_sql) + roles.map(&:authorization_sql).compact + roles.map(&:membership_sql)).join("\n"),
145
+ roles.map(&:name).sort
146
+ )
147
+
148
+ # Migration: Create schemas
149
+ create_migration(
150
+ "create-schemas",
151
+ "Create schemas for containing database objects and controlling access.",
152
+ joined_modobj_sql(schemas, sep: "\nGO\n"),
153
+ schemas.map(&:name).sort
154
+ )
155
+
156
+ tables.each do |table|
157
+ # Migration: Create table
158
+ qual_name = [table.schema, table.name].join('.')
159
+ create_migration(
160
+ "create-table #{qual_name}",
161
+ "Create #{qual_name} table.",
162
+ table.to_sql,
163
+ [table.schema, qual_name]
164
+ )
165
+ end
166
+
167
+ # Migration: Add column defaults
168
+ create_migration(
169
+ "add-column-defaults",
170
+ "Add default constraints to table columns.",
171
+ joined_modobj_sql(column_defaults),
172
+ column_defaults.map {|d| [d.schema, d.qualified_table, d.qualified_column, d.qualified_name].compact}.flatten.uniq.sort
173
+ )
174
+
175
+ # Migration: Add primary key and unique constraints
176
+ create_migration(
177
+ "add-primary-key-and-unique-constraints",
178
+ "Add primary key and unique constraints.",
179
+ joined_modobj_sql(pku_constraints),
180
+ pku_constraints.map {|c| [c.schema, c.qualified_table, c.qualified_name].compact}.flatten.uniq.sort
181
+ )
182
+
183
+ # Migration: Add foreign key constraints
184
+ create_migration(
185
+ "add-foreign-key-constraints",
186
+ "Add foreign key constraints.",
187
+ joined_modobj_sql(foreign_keys),
188
+ foreign_keys.map {|c| [c.schema, c.qualified_table, c.qualified_name].compact}.flatten.uniq.sort
189
+ )
190
+
191
+ # Migration: Add check constraints
192
+ create_migration(
193
+ "add-check-constraints",
194
+ "Add check constraints.",
195
+ joined_modobj_sql(check_constraints),
196
+ check_constraints.map {|c| [c.schema, c.qualified_table, c.qualified_name].compact}.flatten.uniq.sort
197
+ )
198
+
199
+ # Check that no super-permissions reference a view, user-defined function, or stored procedure
200
+ access_object_names = (views + udfs + procedures).map {|ao| ao.qualified_name}
201
+ permissions.map {|p| p.super_permissions}.flatten.select do |p|
202
+ access_object_names.include?(p.target)
203
+ end.group_by {|p| p.target}.tap do |problems|
204
+ raise UnsupportedFeatureError.new(
205
+ "#{problems[0].target} cannot be granted the required permission(s)."
206
+ ) if problems.length == 1
207
+
208
+ raise UnsupportedFeatureError.new(
209
+ (
210
+ ["The required permissions cannot be granted on:"] +
211
+ problems.map {|p| ' ' + p.target}
212
+ ).join("\n")
213
+ ) unless problems.empty?
214
+ end
215
+
216
+ # Write a migration with all super-permissions
217
+ super_permissions = permissions.map {|p| p.super_permissions_sql}.inject([], :concat)
218
+ create_migration(
219
+ "add-super-permissions",
220
+ "Add permissions that confound the normal GRANT model.",
221
+ super_permissions.join("\n"),
222
+ permissions.map {|p| p.super_permissions.map(&:unscoped_target)}.flatten.uniq.sort
223
+ ) unless super_permissions.empty?
224
+
225
+ indexes.each do |index|
226
+ write_index_def(index)
227
+ end
228
+
229
+ write_statistics
230
+
231
+ views.each do |view|
232
+ write_access_def(view, 'view')
233
+ end
234
+
235
+ udfs.each do |udf|
236
+ write_access_def(udf, 'function')
237
+ end
238
+
239
+ procedures.each do |procedure|
240
+ write_access_def(procedure, 'stored procedure')
241
+ end
242
+
243
+ @schema_dir.join(XMigra::SchemaManipulator::PERMISSIONS_FILE).open('w') do |p_file|
244
+ YAML.dump(
245
+ permissions.map do |p|
246
+ p.regular_permissions_graph.map do |k, v|
247
+ [k, {p.subject => v}]
248
+ end.to_h
249
+ end.inject({}) do |r, n|
250
+ r.update(n) {|k, lv, rv| lv.merge rv}
251
+ end,
252
+ p_file
253
+ )
254
+ end
255
+
256
+ create_adoption_script
257
+ end
258
+
259
+ def migration_chain
260
+ @migration_chain ||= XMigra::NewMigrationAdder.new(@schema_dir)
261
+ end
262
+
263
+ def create_migration(summary, description, sql, change_targets)
264
+ migration_chain.add_migration(
265
+ summary,
266
+ description: description,
267
+ sql: sql,
268
+ changes: change_targets
269
+ )
270
+ end
271
+
272
+ def joined_modobj_sql(ary, sep: "\n")
273
+ ary.map(&:to_sql).join(sep)
274
+ end
275
+
276
+ def write_access_def(access_obj, obj_type)
277
+ # Use Psych mid-level emitting API to specify literal syntax for SQL
278
+ def_tree = Psych::Nodes::Mapping.new
279
+ ["define", obj_type, "sql"].each do |s|
280
+ def_tree.children << Psych::Nodes::Scalar.new(s)
281
+ end
282
+ def_tree.children << Psych::Nodes::Scalar.new(access_obj.to_sql, nil, nil, false, true,
283
+ Psych::Nodes::Scalar::LITERAL)
284
+ unless (references = access_obj.respond_to?(:references) ? access_obj.references : []).empty?
285
+ def_tree.children << Psych::Nodes::Scalar.new('referencing')
286
+ def_tree.children << (ref_seq = Psych::Nodes::Sequence.new)
287
+ references.each do |r|
288
+ ref_seq.children << Psych::Nodes::Scalar.new(r)
289
+ end
290
+ end
291
+
292
+ def_doc = Psych::Nodes::Document.new
293
+ def_doc.children << def_tree
294
+ def_stream = Psych::Nodes::Stream.new
295
+ def_stream.children << def_doc
296
+
297
+ access_dir = @schema_dir.join(XMigra::SchemaManipulator::ACCESS_SUBDIR)
298
+ access_dir.mkpath
299
+ access_dir.join(access_obj.qualified_name + '.yaml').open('w') do |ao_file|
300
+ def_str = def_stream.to_yaml(nil, line_width: -1)
301
+ ao_file.puts(def_str)
302
+ end
303
+ end
304
+
305
+ def write_index_def(index)
306
+ indexes_dir = @schema_dir.join(XMigra::SchemaManipulator::INDEXES_SUBDIR)
307
+ indexes_dir.mkpath
308
+ index_path = indexes_dir.join(index.name + '.yaml')
309
+
310
+ raise UnsupportedFeatureError.new(
311
+ "Index file #{index_path} already exists."
312
+ ) if index_path.exist?
313
+
314
+ index_path.open('w') do |index_file|
315
+ YAML.dump({'sql' => index.to_sql}, index_file, line_width: -1)
316
+ end
317
+ end
318
+
319
+ def write_statistics
320
+ statistics_path = @schema_dir.join(XMigra::MSSQLSpecifics::STATISTICS_FILE)
321
+
322
+ statistics_path.open('w') do |stats_file|
323
+ YAML.dump(
324
+ Hash[statistics.map(&:name_params_pair)],
325
+ stats_file,
326
+ line_width: -1
327
+ )
328
+ end
329
+ end
330
+
331
+ def create_adoption_script
332
+ adoption_script_path = @schema_dir.join(ADOPTION_SQL_FILE)
333
+
334
+ writer = AdoptionScriptWriter.new(self)
335
+
336
+ writer.create_script adoption_script_path
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,46 @@
1
+ module Mkxms; end
2
+
3
+ module Mkxms::Mssql
4
+ class DefaultConstraint
5
+ def initialize(schema, table, column, name)
6
+ @schema, @table, @column, @name = schema, table, column, name
7
+ @expression = ''
8
+ end
9
+
10
+ attr_accessor :schema, :table, :column, :name, :expression
11
+
12
+ def to_sql
13
+ "ALTER TABLE #@schema.#@table ADD #{"CONSTRAINT #@name" if @name} DEFAULT #@expression FOR #@column;"
14
+ end
15
+
16
+ def qualified_table
17
+ "#@schema.#@table"
18
+ end
19
+
20
+ def qualified_column
21
+ "#@schema.#@table.#@column"
22
+ end
23
+
24
+ def qualified_name
25
+ "#@schema.#@name" if @name
26
+ end
27
+ end
28
+
29
+ class DefaultConstraintHandler
30
+ def initialize(constraints, node)
31
+ a = node.attributes
32
+ @constraint = DefaultConstraint.new(
33
+ a['schema'],
34
+ a['table'],
35
+ a['column'],
36
+ a['name'],
37
+ ).tap do |c|
38
+ constraints << c
39
+ end
40
+ end
41
+
42
+ def handle_text(text, parent_element)
43
+ @constraint.expression << text
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,88 @@
1
+ require "rexml/document"
2
+ require "rexml/element"
3
+ require "mkxms/mssql/utils"
4
+
5
+ module Mkxms; end
6
+
7
+ module Mkxms::Mssql
8
+ class Mkxms::Mssql::Engine
9
+ ParseItem = Struct.new(:context, :node) do
10
+ def delegate_to(klass, *constructor_args)
11
+ args = constructor_args + [node]
12
+ self.context = klass.new(*args)
13
+ end
14
+ end
15
+
16
+ MissingHandler = Struct.new(:context_class, :handler_name) do
17
+ def to_s
18
+ "#{self.context_class.name} does not define #{self.handler_name}"
19
+ end
20
+ end
21
+
22
+ class ParseErrors < Exception
23
+ def initialize(errors)
24
+ @errors = errors
25
+ super(@errors.map(&:to_s).join("\n"))
26
+ end
27
+
28
+ attr_reader :errors
29
+ end
30
+
31
+ def initialize(document, initial_context)
32
+ @initial_context = initial_context
33
+ @parse_items = [ParseItem.new(initial_context, document.root)]
34
+ @missing_handlers = []
35
+ end
36
+
37
+ attr_reader :missing_handlers
38
+
39
+ def run
40
+ until @parse_items.empty?
41
+ parse_item
42
+ end
43
+
44
+ errors = @missing_handlers
45
+ raise ParseErrors.new(errors) unless errors.empty?
46
+ end
47
+
48
+ private
49
+ def parse_item
50
+ item = @parse_items.shift
51
+ case item.node
52
+ when REXML::Element
53
+ begin
54
+ handler = item.context.method(handler_name = element_handler_method_name(item.node))
55
+ rescue NameError
56
+ record_missing_handler(item.context.class, handler_name)
57
+ return
58
+ end
59
+ result = ParseItem.new(item.context, item.node)
60
+ handler[result]
61
+ @parse_items = item.node.children.select do |node|
62
+ [REXML::Element, REXML::Text].any? {|c| node.kind_of? c}
63
+ end.map do |node|
64
+ ParseItem.new(result.context, node)
65
+ end + @parse_items
66
+ when REXML::Text
67
+ begin
68
+ handler = item.context.method(:handle_text)
69
+ rescue
70
+ record_missing_handler(item.context.class, :handle_text) unless item.node.value =~ /^\s*$/
71
+ return
72
+ end
73
+ handler[item.node.value, item.node.parent]
74
+ end
75
+ end
76
+
77
+ def element_handler_method_name(node)
78
+ case node
79
+ when REXML::Element
80
+ "handle_#{Utils.code_sym_for node.name}_element".to_sym
81
+ end
82
+ end
83
+
84
+ def record_missing_handler(context_class, method_name)
85
+ @missing_handlers << MissingHandler.new(context_class, method_name)
86
+ end
87
+ end
88
+ end