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,11 @@
1
+ module Mkxms; end
2
+ module Mkxms::Mssql; end
3
+
4
+ Mkxms::Mssql.const_set(
5
+ :IndexColumn,
6
+ Struct.new(:name, :direction) do
7
+ def to_sql
8
+ "#{name} #{direction == :descending ? 'DESC' : 'ASC'}"
9
+ end
10
+ end
11
+ )
@@ -0,0 +1,98 @@
1
+ require 'mkxms/mssql/property_handler'
2
+ require 'mkxms/mssql/utils'
3
+
4
+ module Mkxms; end
5
+
6
+ module Mkxms::Mssql
7
+ class Index
8
+ extend Utils::FlagsQueries
9
+ include ExtendedProperties, Property::Hosting
10
+
11
+ def initialize(attrs)
12
+ @schema = attrs['schema']
13
+ @relation = attrs['relation']
14
+ @name = attrs['name']
15
+ @fill_factor = attrs['fill-factor']
16
+ @spatial_index_geometry = attrs['spatial-index-over']
17
+ @cells_per_object = attrs['cells-per-object']
18
+ @storage = attrs['stored-on']
19
+ @columns = []
20
+ @included_columns = []
21
+
22
+ @flags = []
23
+ @flags << :unique if attrs['unique']
24
+ @flags << :padded if attrs['padded']
25
+ @flags << :disabled if attrs['disabled']
26
+ @flags << :ignore_duplicates if attrs['ignore-duplicates']
27
+ @flags << :row_locks_prohibited if attrs['no-row-locks']
28
+ @flags << :page_locks_prohibited if attrs['no-page-locks']
29
+ end
30
+
31
+ attr_accessor :schema, :relation, :name, :fill_factor, :spatial_index_geometry, :cells_per_object, :storage
32
+ attr_reader :columns, :included_columns, :flags
33
+
34
+ flags_query :unique, :padded, :ignore_duplicates, :row_locks_prohibited, :page_locks_prohibited
35
+
36
+ def to_sql
37
+ if @spatial_index_geometry
38
+ else
39
+ [].tap do |parts|
40
+ parts << "CREATE #{'UNIQUE ' if unique?}INDEX #@name ON #{qualified_relation} (\n" +
41
+ @columns.map(&:to_sql).join(', ') +
42
+ "\n)"
43
+
44
+ parts << "INCLUDE (\n" +
45
+ @included_columns.map(&:name).join(', ') +
46
+ "\n)" unless @included_columns.empty?
47
+
48
+ # TODO: "WHERE" clause
49
+
50
+ options = []
51
+ options << "PAD_INDEX = ON" if padded?
52
+ options << "FILLFACTOR = #@fill_factor" if @fill_factor
53
+ options << "IGNORE_DUP_KEY = ON" if ignore_duplicates?
54
+ options << "ALLOW_ROW_LOCKS = OFF" if row_locks_prohibited?
55
+ options << "ALLOW_PAGE_LOCKS = OFF" if page_locks_prohibited?
56
+ parts << "WITH (#{options.join(', ')})" unless options.empty?
57
+
58
+ parts << "ON #@storage" if @storage
59
+
60
+ end.join(' ') + ';' + extended_properties_sql.joined_on_new_lines
61
+ end
62
+ end
63
+
64
+ def property_subject_identifiers
65
+ ['SCHEMA', @schema, 'TABLE', @relation, 'INDEX', @name].map {|n| Utils.unquoted_name(n)}
66
+ end
67
+
68
+ def qualified_relation
69
+ [@schema, @relation].join '.'
70
+ end
71
+ end
72
+
73
+ class IndexHandler
74
+ include PropertyHandler::ElementHandler
75
+
76
+ def initialize(indexes, node)
77
+ @index = Index.new(node.attributes).tap do |i|
78
+ indexes << i
79
+ end
80
+ end
81
+
82
+ def extended_properties
83
+ @index.extended_properties
84
+ end
85
+
86
+ def handle_column_element(parse)
87
+ a = parse.node.attributes
88
+
89
+ if a['included']
90
+ @index.included_columns << IndexColumn.new(a['name'])
91
+ else
92
+ @index.columns << IndexColumn.new(a['name'], a['desc'] ? :descending : :ascending)
93
+ end
94
+ end
95
+
96
+ # TODO: Handle partition columns
97
+ end
98
+ end
@@ -0,0 +1,67 @@
1
+ require 'mkxms/mssql/index_column'
2
+ require 'mkxms/mssql/property_handler'
3
+ require 'mkxms/mssql/utils'
4
+
5
+ module Mkxms; end
6
+
7
+ module Mkxms::Mssql
8
+ class KeylikeConstraint
9
+ extend Utils::FlagsQueries
10
+ include ExtendedProperties, Property::Hosting
11
+
12
+ def initialize(attrs)
13
+ @schema = attrs['schema']
14
+ @table = attrs['table']
15
+ @name = attrs['name']
16
+ @stored_on = attrs['stored-on']
17
+ @fill_factor = attrs['fill-factor']
18
+
19
+ @flags = []
20
+ @flags << :clustered if attrs['clustered']
21
+ @flags << :paddedd if attrs['padded']
22
+ @flags << :row_locks_ok unless attrs['no-row-locks']
23
+ @flags << :page_locks_ok unless attrs['no-page-locks']
24
+
25
+ @columns = []
26
+ end
27
+
28
+ attr_accessor :schema, :table, :name, :stored_on, :fill_factor
29
+ attr_reader :columns, :flags
30
+ flags_query :clustered, :padded, :row_locks_ok, :page_locks_ok
31
+
32
+ def to_sql
33
+ "ALTER TABLE #@schema.#@table ADD #{"CONSTRAINT #@name " if @name}" +
34
+ "#{self.sql_constraint_type} #{'NON' unless clustered?}CLUSTERED (\n" +
35
+ ' ' + columns.map {|c| c.to_sql}.join(", ") +
36
+ "\n)" +
37
+ with_clause_sql +
38
+ # TODO: Handle partitioned constraints
39
+ "#{" ON #@stored_on" if @stored_on}" +
40
+ ";" +
41
+ (name ? extended_properties_sql.joined_on_new_lines : '')
42
+ end
43
+
44
+ def with_clause_sql
45
+ options = []
46
+ options << 'PAD_INDEX = ON' if padded?
47
+ options << "FILLFACTOR = #@fill_factor" if fill_factor
48
+ options << 'ALLOW_ROW_LOCKS = OFF' unless row_locks_ok?
49
+ options << 'ALLOW_PAGE_LOCKS = OFF' unless page_locks_ok?
50
+
51
+ return '' if options.empty?
52
+ return " WITH (\n#{options.join ", "}\n)"
53
+ end
54
+
55
+ def qualified_table
56
+ "#@schema.#@table"
57
+ end
58
+
59
+ def qualified_name
60
+ "#@schema.#@name" if @name
61
+ end
62
+
63
+ def property_subject_identifiers
64
+ @prop_subj_id ||= ['SCHEMA', schema, 'TABLE', table, 'CONSTRAINT', name].map {|s| Utils::unquoted_name(s)}
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,115 @@
1
+ require 'mkxms/mssql/utils'
2
+
3
+ module Mkxms; end
4
+
5
+ module Mkxms::Mssql
6
+ class PermissionGroup
7
+ ACTION_STATEMENT_PROLOG_TEMPLATES = {
8
+ 'granted' => 'GRANT %s ON %s TO %s',
9
+ 'denied' => 'DENY %s ON %s TO %s',
10
+ }
11
+
12
+ def initialize(action, subject)
13
+ @action = action
14
+ @subject = subject
15
+ @permissions = []
16
+ end
17
+
18
+ attr_accessor :action, :subject
19
+ attr_reader :permissions
20
+
21
+ def super_permissions_sql
22
+ super_permissions.map do |p|
23
+ ''.tap do |sql|
24
+ sql << ACTION_STATEMENT_PROLOG_TEMPLATES[action] % [p.name, p.target, subject]
25
+ sql << ' WITH GRANT OPTION' if p.grant_option?
26
+ sql << ';'
27
+ end
28
+ end
29
+ end
30
+
31
+ def regular_permissions_graph
32
+ Hash.new.tap do |result|
33
+ regular_permissions.sort {|a, b| a.target <=> b.target}.group_by {|p| p.target}.each_pair do |target, perms|
34
+ result[target] = perms.map(&:name)
35
+ end
36
+ end
37
+ end
38
+
39
+ def is_super_permission?(p)
40
+ action != 'granted' || p.grant_option?
41
+ end
42
+
43
+ def super_permissions
44
+ permissions.select {|p| is_super_permission? p}
45
+ end
46
+
47
+ def regular_permissions
48
+ permissions.select {|p| !is_super_permission? p}
49
+ end
50
+ end
51
+
52
+ class Permission
53
+ def initialize(attrs)
54
+ @name = attrs['name']
55
+ @target_type = attrs['target-type']
56
+ @name_scope = attrs['name-scope']
57
+ @schema = attrs['in-schema']
58
+ @object = attrs['on']
59
+ @column = attrs['column']
60
+ @target = if @object
61
+ "".tap do |subject|
62
+ if @schema
63
+ subject << (@schema + '.')
64
+ end
65
+ subject << @object
66
+ subject << " (#@column)" if @column
67
+ end
68
+ else
69
+ 'DATABASE'
70
+ end
71
+ @grant_option = attrs['with-grant-option']
72
+ @authority = attrs['by']
73
+ end
74
+
75
+ attr_accessor :name, :target_type, :name_scope, :column, :authority
76
+
77
+ def target(scoped: true)
78
+ if scoped && @name_scope
79
+ "#@name_scope :: #@target"
80
+ else
81
+ @target
82
+ end
83
+ end
84
+
85
+ def unscoped_target
86
+ target(scoped: false)
87
+ end
88
+
89
+ def grant_option?
90
+ @grant_option
91
+ end
92
+ def grant_option=(value)
93
+ @grant_option = value
94
+ end
95
+
96
+ def object_id_parts
97
+ [@target_type, @schema, @object, @column]
98
+ end
99
+ end
100
+
101
+ class PermissionHandler
102
+ def initialize(permissions, node)
103
+ a = node.attributes
104
+
105
+ @action = PermissionGroup.new(node.name, a['to'] || a['from']).tap do |pg|
106
+ permissions << pg
107
+ end
108
+ end
109
+
110
+ def handle_permission_element(parse)
111
+ a = parse.node.attributes
112
+ @action.permissions << Permission.new(a)
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,36 @@
1
+ require 'mkxms/mssql/keylike_constraint_helper'
2
+
3
+ module Mkxms::Mssql
4
+ class PrimaryKey < Mkxms::Mssql::KeylikeConstraint
5
+ SQL_CONSTRAINT_TYPE = 'PRIMARY KEY'
6
+
7
+ def sql_constraint_type
8
+ SQL_CONSTRAINT_TYPE
9
+ end
10
+ end
11
+
12
+ class PrimaryKeyHandler
13
+ include PropertyHandler::ElementHandler
14
+
15
+ def initialize(constraints, node)
16
+ a = node.attributes
17
+
18
+ @pkey = PrimaryKey.new(a).tap do |c|
19
+ constraints << c
20
+ end
21
+ end
22
+
23
+ def extended_properties
24
+ @pkey.extended_properties
25
+ end
26
+
27
+ def handle_column_element(parse)
28
+ a = parse.node.attributes
29
+
30
+ raise UnsupportedFeatureError.new("Primary keys may not specify included columns (#{@pkey.qualified_table})") if a['included']
31
+ @pkey.columns << IndexColumn.new(a['name'], a['desc'] ? :descending : :ascending)
32
+ end
33
+
34
+ # TODO: Handle partitioned primary keys
35
+ end
36
+ end
@@ -0,0 +1,87 @@
1
+ require 'base64'
2
+ require 'mkxms/mssql/utils'
3
+
4
+ module Mkxms; end
5
+
6
+ module Mkxms::Mssql
7
+ module ExtendedProperties
8
+ def extended_properties
9
+ @extended_properties ||= {}
10
+ end
11
+ end
12
+
13
+ module Property
14
+ def self.addition_sql(name, value, subject_identification_parts)
15
+ "EXEC sp_addextendedproperty N'%s', %s, %s;" % [
16
+ name,
17
+ value,
18
+ subject_identification_parts.map {|part| "N'#{part}'"}.join(', ')
19
+ ]
20
+ end
21
+
22
+ module Hosting
23
+ def extended_properties_sql
24
+ self.extended_properties.each_pair.map do |name, value|
25
+ Mkxms::Mssql::Property.addition_sql(name, value, self.property_subject_identifiers)
26
+ end.tap do |v|
27
+ class <<v
28
+ def joined_on_new_lines(indent: ' ')
29
+ map {|i| "\n" + indent + i}.join('')
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ module SchemaScoped
37
+ def property_subject_identifiers
38
+ ['SCHEMA', Utils::unquoted_name(schema), self.class::SQL_OBJECT_TYPE.upcase, Utils.unquoted_name(name)]
39
+ end
40
+
41
+ def subitem_extended_properties_sql(subitem)
42
+ subitem.extended_properties.each_pair.map do |name, value|
43
+ Mkxms::Mssql::Property.addition_sql(
44
+ name, value,
45
+ property_subject_identifiers + [subitem.class::SQL_OBJECT_TYPE.upcase, Utils.unquoted_name(subitem.name)]
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ class PropertyHandler
53
+ module ElementHandler
54
+ def handle_property_element(parse)
55
+ parse.context = PropertyHandler.new(self, parse.node.attributes)
56
+ end
57
+ end
58
+
59
+ def initialize(describable, attrs)
60
+ @describable = describable
61
+ @name = attrs['name']
62
+ @value_type = attrs['type'].downcase
63
+ end
64
+
65
+ def handle_text(property_value, node)
66
+ stored_value = property_value.dup
67
+
68
+ stored_value = Base64.decode64(stored_value) if @value_type.include? 'binary'
69
+
70
+ stored_value.define_singleton_method(
71
+ :to_sql_literal,
72
+ &(case @value_type
73
+ when 'char', 'varchar', 'uniqueidentifier', 'smalldatetime', 'datetime'
74
+ ->() {"'#{self}'"}
75
+ when 'nchar', 'nvarchar'
76
+ ->() {"N'#{self}'"}
77
+ when 'binary', 'varbinary'
78
+ ->() {"0x" + self.bytes.map {|b| "%02x" % b}.join}
79
+ else
80
+ ->() {self.to_s}
81
+ end)
82
+ )
83
+
84
+ @describable.extended_properties[@name] = stored_value
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,111 @@
1
+ module Mkxms; end
2
+
3
+ module Mkxms::Mssql
4
+ class QueryCursor
5
+ def initialize(select_statement, variables, options = {})
6
+ @select_statement = select_statement
7
+ @select_statement += ';' unless @select_statement.end_with? ';'
8
+ @cursor = options[:cursor_name] || self.class.generated_cursor_name
9
+ @out = options[:output_to] || $stdout
10
+ @global = options[:global]
11
+ @indent = options[:indent] || ' '
12
+
13
+ @variable_decl = variables.gsub(/\s+/, ' ')
14
+ @variable_names = variables.split(',').map do |vardecl|
15
+ vardecl.chomp.split(nil, 2)[0]
16
+ end
17
+ end
18
+
19
+ attr_reader :cursor_name
20
+
21
+ def each_row
22
+ set_up_loop
23
+ fetch_next
24
+ @out.puts "WHILE @@FETCH_STATUS = 0"
25
+ @out.puts "BEGIN"
26
+ yield
27
+ fetch_next(@indent)
28
+ @out.puts "END;"
29
+ end
30
+
31
+ class ExpectedRowTest
32
+ def initialize(test_proc)
33
+ @test_proc = test_proc
34
+ end
35
+
36
+ def row(*args, &blk)
37
+ @test_proc.call(*args, &blk)
38
+ end
39
+ end
40
+
41
+ def expectations(opts = {})
42
+ extra_action = expectation_failure_action(opts[:on_extra])
43
+ test_entry_proc = if missing_action = expectation_failure_action(opts[:on_missing])
44
+ proc {|&blk| test_entry(on_missing: missing_action, &blk)}
45
+ else
46
+ method(:test_entry)
47
+ end
48
+
49
+ set_up_loop
50
+
51
+ yield ExpectedRowTest.new(test_entry_proc)
52
+
53
+ if extra_action
54
+ fetch_next
55
+ @out.puts "IF @@FETCH_STATUS = 0"
56
+ @out.puts "BEGIN"
57
+ extra_action.call
58
+ @out.puts "END;"
59
+ end
60
+
61
+ tear_down_loop
62
+ end
63
+
64
+ def test_entry(opts = {})
65
+ opts = {} unless opts.kind_of? Hash
66
+ missing_action = expectation_failure_action(opts[:on_missing]) || proc {}
67
+ fetch_next
68
+ @out.puts "IF @@FETCH_STATUS <> 0"
69
+ @out.puts "BEGIN"
70
+ missing_action.call
71
+ @out.puts "END ELSE BEGIN"
72
+ yield
73
+ @out.puts "END;"
74
+ end
75
+
76
+ def expectation_failure_action(value)
77
+ case value
78
+ when Proc then value
79
+ when String then proc {@out.puts(@indent + value)}
80
+ end
81
+ end
82
+
83
+ def cursor_scope(explicit_local = true)
84
+ case
85
+ when @global then 'GLOBAL'
86
+ when explicit_local then 'LOCAL'
87
+ else ''
88
+ end
89
+ end
90
+
91
+ def set_up_loop
92
+ @out.puts "DECLARE #@variable_decl;"
93
+ @out.puts "DECLARE #@cursor CURSOR #{cursor_scope} FOR"
94
+ @out.puts @select_statement
95
+ @out.puts "OPEN #@cursor;"
96
+ end
97
+
98
+ def fetch_next(indent = '')
99
+ @out.puts(indent + "FETCH NEXT FROM #@cursor INTO #{@variable_names.join(', ')};")
100
+ end
101
+
102
+ def tear_down_loop
103
+ @out.puts "CLOSE #{cursor_scope(false)} #@cursor; DEALLOCATE #{cursor_scope(false)} #@cursor;"
104
+ end
105
+
106
+ def self.generated_cursor_name
107
+ @gensym_number ||= 0
108
+ "gensym_cursor_#{@gensym_number += 1}"
109
+ end
110
+ end
111
+ end