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,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