mkxms-mssql 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +53 -0
  6. data/Rakefile +1 -0
  7. data/bin/mkxms-mssql +5 -0
  8. data/lib/mkxms/mssql/access_object_definition.rb +61 -0
  9. data/lib/mkxms/mssql/adoption_script_writer.rb +1486 -0
  10. data/lib/mkxms/mssql/check_constraint_handler.rb +56 -0
  11. data/lib/mkxms/mssql/database_handler.rb +339 -0
  12. data/lib/mkxms/mssql/default_constraint_handler.rb +46 -0
  13. data/lib/mkxms/mssql/engine.rb +88 -0
  14. data/lib/mkxms/mssql/exceptions.rb +10 -0
  15. data/lib/mkxms/mssql/filegroup_handler.rb +81 -0
  16. data/lib/mkxms/mssql/foreign_key_handler.rb +85 -0
  17. data/lib/mkxms/mssql/function_handler.rb +74 -0
  18. data/lib/mkxms/mssql/indented_string_builder.rb +199 -0
  19. data/lib/mkxms/mssql/index_column.rb +11 -0
  20. data/lib/mkxms/mssql/index_handler.rb +98 -0
  21. data/lib/mkxms/mssql/keylike_constraint_helper.rb +67 -0
  22. data/lib/mkxms/mssql/permission_handler.rb +115 -0
  23. data/lib/mkxms/mssql/primary_key_handler.rb +36 -0
  24. data/lib/mkxms/mssql/property_handler.rb +87 -0
  25. data/lib/mkxms/mssql/query_cursor.rb +111 -0
  26. data/lib/mkxms/mssql/role_handler.rb +55 -0
  27. data/lib/mkxms/mssql/schema_handler.rb +42 -0
  28. data/lib/mkxms/mssql/sql_string_manipulators.rb +46 -0
  29. data/lib/mkxms/mssql/statistics_handler.rb +59 -0
  30. data/lib/mkxms/mssql/stored_procedure_handler.rb +65 -0
  31. data/lib/mkxms/mssql/table_handler.rb +180 -0
  32. data/lib/mkxms/mssql/unique_constraint_handler.rb +32 -0
  33. data/lib/mkxms/mssql/utils.rb +83 -0
  34. data/lib/mkxms/mssql/version.rb +5 -0
  35. data/lib/mkxms/mssql/view_handler.rb +58 -0
  36. data/lib/mkxms/mssql.rb +62 -0
  37. data/mkxms-mssql.gemspec +26 -0
  38. data/spec/utils/indented_string_builder_spec.rb +218 -0
  39. data/spec/utils/query_cursor_spec.rb +57 -0
  40. metadata +142 -0
@@ -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