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
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'mkxms/mssql/keylike_constraint_helper'
|
2
|
+
|
3
|
+
module Mkxms; end
|
4
|
+
|
5
|
+
module Mkxms::Mssql
|
6
|
+
class UniqueConstraint < KeylikeConstraint
|
7
|
+
SQL_CONSTRAINT_TYPE = 'UNIQUE'
|
8
|
+
|
9
|
+
def sql_constraint_type
|
10
|
+
SQL_CONSTRAINT_TYPE
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class UniqueConstraintHandler
|
15
|
+
def initialize(constraints, node)
|
16
|
+
a = node.attributes
|
17
|
+
|
18
|
+
@uconst = UniqueConstraint.new(a).tap do |c|
|
19
|
+
constraints << c
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def handle_column_element(parse)
|
24
|
+
a = parse.node.attributes
|
25
|
+
|
26
|
+
raise UnsupportedFeatureError.new("Unique constraints may not specify included columns (#{@uconst.qualified_table})") if a['included']
|
27
|
+
@uconst.columns << IndexColumn.new(a['name'], a['desc'] ? :descending : :ascending)
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO: Handle partitioned unique constraints
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
|
3
|
+
module Mkxms; end
|
4
|
+
module Mkxms::Mssql; end
|
5
|
+
|
6
|
+
module Mkxms::Mssql::Utils
|
7
|
+
INVALID_NAME_CHAR = /[^A-Za-z0-9_]/
|
8
|
+
|
9
|
+
module FlagsQueries
|
10
|
+
def flags_query(*syms, flags: :flags)
|
11
|
+
syms.each do |sym|
|
12
|
+
define_method((sym.to_s + '?').to_sym) {send(flags).include? sym}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module InitializedAttributes
|
18
|
+
def attr_init(*syms, &blk)
|
19
|
+
raise "No block given for initialization of attr_init" unless blk
|
20
|
+
|
21
|
+
syms.each do |sym|
|
22
|
+
inst_var = "@#{sym}".to_sym
|
23
|
+
define_method(sym) do
|
24
|
+
instance_variable_get(inst_var) ||
|
25
|
+
instance_variable_set(inst_var, blk[])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module SchemaQualifiedName
|
32
|
+
def qualified_name
|
33
|
+
[schema, name].join('.')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class NameRefGraph
|
38
|
+
include TSort
|
39
|
+
|
40
|
+
def initialize(items, children: :children)
|
41
|
+
@items = items
|
42
|
+
@children_message = children
|
43
|
+
end
|
44
|
+
|
45
|
+
def tsort_each_node(&blk)
|
46
|
+
@items.each(&blk)
|
47
|
+
end
|
48
|
+
|
49
|
+
def tsort_each_child(item, &blk)
|
50
|
+
item.send(@children_message).each(&blk)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class << Mkxms::Mssql::Utils
|
56
|
+
def code_sym_for(s)
|
57
|
+
s.gsub(self::INVALID_NAME_CHAR, '_').downcase.to_sym
|
58
|
+
end
|
59
|
+
|
60
|
+
def unquoted_name(s)
|
61
|
+
return s unless s[0] == '[' && s[-1] == ']'
|
62
|
+
return s[1...-1].gsub(']]', ']')
|
63
|
+
end
|
64
|
+
|
65
|
+
def newline_prefixed(s)
|
66
|
+
"\n" + s
|
67
|
+
end
|
68
|
+
|
69
|
+
def chars_to_tab(prev, tab_width: 4)
|
70
|
+
(prev.chars.length + 3) % 4 + 1
|
71
|
+
end
|
72
|
+
|
73
|
+
def expand_tabs(s, tab_width: 4)
|
74
|
+
return s unless s.include? "\t"
|
75
|
+
|
76
|
+
s.each_line.map do |l|
|
77
|
+
while l.include? "\t"
|
78
|
+
l.sub!("\t") {|m| ' ' * chars_to_tab($`, tab_width: tab_width)}
|
79
|
+
end
|
80
|
+
l
|
81
|
+
end.join('')
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'mkxms/mssql/access_object_definition'
|
2
|
+
require 'mkxms/mssql/property_handler'
|
3
|
+
require 'mkxms/mssql/utils'
|
4
|
+
|
5
|
+
module Mkxms; end
|
6
|
+
|
7
|
+
module Mkxms::Mssql
|
8
|
+
class View
|
9
|
+
include ExtendedProperties, Property::Hosting, Property::SchemaScoped
|
10
|
+
include Utils::SchemaQualifiedName
|
11
|
+
|
12
|
+
SQL_OBJECT_TYPE = 'VIEW'
|
13
|
+
|
14
|
+
def initialize(attrs)
|
15
|
+
@schema = attrs['schema']
|
16
|
+
@name = attrs['name']
|
17
|
+
@definition = ''
|
18
|
+
@references = []
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :schema, :name
|
22
|
+
attr_reader :definition, :references
|
23
|
+
|
24
|
+
def to_sql
|
25
|
+
mvdef = AccessObjectDefinition.replace_object_name(definition, "[{filename}]")
|
26
|
+
([mvdef] + extended_properties_sql).join("\n")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class ViewHandler
|
31
|
+
include PropertyHandler::ElementHandler
|
32
|
+
|
33
|
+
def initialize(views, node)
|
34
|
+
a = node.attributes
|
35
|
+
|
36
|
+
@view = View.new(a).tap do |v|
|
37
|
+
views << v
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def extended_properties
|
42
|
+
@view.extended_properties
|
43
|
+
end
|
44
|
+
|
45
|
+
def handle_definition_element(parse); end
|
46
|
+
|
47
|
+
def handle_references_element(parse)
|
48
|
+
@view.references << %w[schema name].map {|k| parse.node.attributes[k]}.join('.')
|
49
|
+
end
|
50
|
+
|
51
|
+
def handle_text(text, parent_element)
|
52
|
+
case [parent_element.namespace, parent_element.name]
|
53
|
+
when ['', 'definition']
|
54
|
+
@view.definition << text
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/mkxms/mssql.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require "optparse"
|
2
|
+
require "ostruct"
|
3
|
+
require "pathname"
|
4
|
+
require "rexml/document"
|
5
|
+
|
6
|
+
module Mkxms
|
7
|
+
module Mssql
|
8
|
+
def self.parse_argv(argv = ARGV.dup)
|
9
|
+
options = OpenStruct.new
|
10
|
+
optparser = OptionParser.new do |flags|
|
11
|
+
flags.banner = "Usage: #{File.basename($0)} [<option> [<option> ...]] [DB_DESCRIPTION_FILE]"
|
12
|
+
flags.separator ''
|
13
|
+
flags.separator 'Options:'
|
14
|
+
|
15
|
+
options.schema_dir = Pathname.pwd
|
16
|
+
flags.on('-o', '--outdir=SCHEMA_DIR', "Output in SCHEMA_DIR") do |schema_dir|
|
17
|
+
options.schema_dir = Pathname(schema_dir).expand_path
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
db_files = optparser.parse(argv)
|
22
|
+
case db_files.length
|
23
|
+
when ->(n) {n > 1}
|
24
|
+
"Too many DB_DESCRIPTION_FILEs given"
|
25
|
+
end.tap {|msg| raise ProgramArgumentError.new(msg) if msg}
|
26
|
+
|
27
|
+
return [db_files[0], options]
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.generate_from(document, options)
|
31
|
+
db_handler = DatabaseHandler.new(**(options.to_h))
|
32
|
+
engine = Engine.new(document, db_handler)
|
33
|
+
engine.run
|
34
|
+
db_handler.create_source_files
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.with_db_description_io(file_path, &blk)
|
38
|
+
if file_path
|
39
|
+
File.open(file_path, 'r', &blk)
|
40
|
+
else
|
41
|
+
blk.call($stdin)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.run_program(argv = ARGV.dup)
|
46
|
+
description_file, options = parse_argv(argv)
|
47
|
+
document = with_db_description_io(description_file) do |xml_file|
|
48
|
+
REXML::Document.new(xml_file)
|
49
|
+
end
|
50
|
+
generate_from(document, options)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
require "mkxms/mssql/database_handler"
|
56
|
+
require "mkxms/mssql/engine"
|
57
|
+
require "mkxms/mssql/exceptions"
|
58
|
+
require "mkxms/mssql/version"
|
59
|
+
|
60
|
+
if __FILE__.eql? $0
|
61
|
+
Mkxms::Mssql.run_program
|
62
|
+
end
|
data/mkxms-mssql.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mkxms/mssql/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "mkxms-mssql"
|
8
|
+
spec.version = Mkxms::Mssql::VERSION
|
9
|
+
spec.authors = ["Richard Weeks"]
|
10
|
+
spec.email = ["rtweeks21@gmail.com"]
|
11
|
+
spec.summary = %q{XMigra source files from MS-SQL database description.}
|
12
|
+
spec.description = %q{Build a complete set of XMigra source files from an XML document (as produced by the mssql-eyewkas.sql script) describing an MS-SQL database.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency "xmigra", '~> 1.1'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
26
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'mkxms/mssql/indented_string_builder'
|
2
|
+
|
3
|
+
class RSpec::Expectations::ExpectationTarget
|
4
|
+
def to_have(length_expectation)
|
5
|
+
(match = Object.new).extend RSpec::Matchers
|
6
|
+
to match.have length_expectation
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_not_have(length_expectation)
|
10
|
+
(match = Object.new).extend RSpec::Matchers
|
11
|
+
to_not match.have length_expectation
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
RSpec::Matchers.define :have do |expected|
|
16
|
+
match do |actual|
|
17
|
+
actual.length == expected.count
|
18
|
+
end
|
19
|
+
failure_message do |actual|
|
20
|
+
"expected #{actual} to have #{expected.count} items"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
RSpec::Expectations::LengthExpectation = Struct.new(:count)
|
25
|
+
class Integer
|
26
|
+
def items
|
27
|
+
RSpec::Expectations::LengthExpectation.new(self)
|
28
|
+
end
|
29
|
+
|
30
|
+
def item
|
31
|
+
raise "Numericity mismatch" unless self == 1
|
32
|
+
RSpec::Expectations::LengthExpectation.new(1)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe Mkxms::Mssql::IndentedStringBuilder do
|
37
|
+
def check_contractor_example(s)
|
38
|
+
lines = s.to_s.lines
|
39
|
+
expect(lines).to_have 4.items
|
40
|
+
["SELECT", " name", " AND trade", ";"].each.with_index do |line_start, i|
|
41
|
+
expect(lines[i]).to start_with(line_start)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "can print a single line like a regular StringIO" do
|
46
|
+
subject.puts("Hello world!")
|
47
|
+
expect(subject.to_s.lines).to_have 1.item
|
48
|
+
end
|
49
|
+
|
50
|
+
it "can substitute into a line" do
|
51
|
+
sub = "happy"
|
52
|
+
subject.puts("I'm feeling %s.") {sub}
|
53
|
+
expect(subject.to_s).to include(sub)
|
54
|
+
expect(subject.to_s.lines).to_have 1.item
|
55
|
+
end
|
56
|
+
|
57
|
+
it "can substitue a value that is not a string into a template" do
|
58
|
+
sub = 17
|
59
|
+
subject.puts("I would like %s candy bars.") {sub}
|
60
|
+
expect(subject.to_s).to include(sub.to_s)
|
61
|
+
expect(subject.to_s.lines).to_have 1.item
|
62
|
+
end
|
63
|
+
|
64
|
+
it "can substitute an indented section" do
|
65
|
+
conditions = "name = 'Joe'\nAND trade='plumber'"
|
66
|
+
subject.puts("SELECT * FROM contractors WHERE %s;", :each_indent=>' ') {conditions}
|
67
|
+
expect(subject.to_s.lines).to_have 4.items
|
68
|
+
check_contractor_example(subject)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "can #puts without substitution" do
|
72
|
+
subject.puts("weird %stuff", :sub => nil)
|
73
|
+
expect(subject.to_s).to eql("weird %stuff\n")
|
74
|
+
end
|
75
|
+
|
76
|
+
context "begin/end template" do
|
77
|
+
let(:template) {"BEGIN".."END"}
|
78
|
+
|
79
|
+
it "substitutes a single line correctly" do
|
80
|
+
subject.puts(template) {"body"}
|
81
|
+
expect(subject.to_s).to eql("BEGIN\n body\nEND\n")
|
82
|
+
end
|
83
|
+
|
84
|
+
it "substitutes multiple lines correctly" do
|
85
|
+
subject.puts(template) {"body1\nbody2"}
|
86
|
+
expect(subject.to_s).to eql("BEGIN\n body1\n body2\nEND\n")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
it "yields the matched name if the :sub option is :named" do
|
91
|
+
sub_indicators = []
|
92
|
+
subject.puts("Eeny {meeny} {miney} mo", :sub => :named) {|insert|
|
93
|
+
sub_indicators << insert
|
94
|
+
"woot!"
|
95
|
+
}
|
96
|
+
expect(sub_indicators).to include('meeny')
|
97
|
+
expect(sub_indicators).to include('miney')
|
98
|
+
end
|
99
|
+
|
100
|
+
context "DSL" do
|
101
|
+
it "reroutes #puts to the builder (not $stdout)" do
|
102
|
+
expect do
|
103
|
+
subject.dsl {
|
104
|
+
puts "Hello, world!"
|
105
|
+
}
|
106
|
+
end.not_to output.to_stdout
|
107
|
+
expect(subject.to_s).to eql("Hello, world!\n")
|
108
|
+
end
|
109
|
+
|
110
|
+
it "provides for indented blocks" do
|
111
|
+
subject.dsl {
|
112
|
+
puts "SELECT * FROM contractors WHERE"
|
113
|
+
indented {
|
114
|
+
puts "name = 'Joe'"
|
115
|
+
puts "AND trade = 'plumber'"
|
116
|
+
}
|
117
|
+
puts ";"
|
118
|
+
}
|
119
|
+
check_contractor_example(subject)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "allows indented injection with #puts" do
|
123
|
+
subject.dsl {
|
124
|
+
puts "SELECT * FROM contractors WHERE %s;" do
|
125
|
+
puts "name = 'Joe'"
|
126
|
+
puts "AND trade = 'plumber'"
|
127
|
+
end
|
128
|
+
}
|
129
|
+
check_contractor_example(subject)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "captures variables from surrounding binding" do
|
133
|
+
the_answer = 42
|
134
|
+
subject.dsl {
|
135
|
+
puts "The answer to the ultimate question of life, the universe, and everything is #{the_answer}."
|
136
|
+
}
|
137
|
+
expect(subject.to_s).to include(the_answer.to_s)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "allows calls to methods defined in the surrounding binding" do
|
141
|
+
def blargize(s)
|
142
|
+
s + "blarg"
|
143
|
+
end
|
144
|
+
|
145
|
+
subject.dsl {
|
146
|
+
puts "All answers exist on the #{blargize 'web'}."
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
it "does not capture output to $stdout within methods of the surrounding binding" do
|
151
|
+
def real_puts(s)
|
152
|
+
puts s
|
153
|
+
end
|
154
|
+
|
155
|
+
expect {
|
156
|
+
subject.dsl {
|
157
|
+
real_puts "This goes out!"
|
158
|
+
}
|
159
|
+
}.to output.to_stdout
|
160
|
+
end
|
161
|
+
|
162
|
+
it "is available from the class and returns a string" do
|
163
|
+
class_dsl_result = subject.class.dsl {
|
164
|
+
puts "Hello, world!"
|
165
|
+
}
|
166
|
+
|
167
|
+
expect(class_dsl_result).to eql("Hello, world!\n")
|
168
|
+
end
|
169
|
+
|
170
|
+
context "in subclass" do
|
171
|
+
it "properly handles calls to methods nested within DSL" do
|
172
|
+
test_class = Class.new(subject.class) do
|
173
|
+
def initialize
|
174
|
+
super
|
175
|
+
|
176
|
+
dsl {
|
177
|
+
puts "BEGIN"
|
178
|
+
indented {
|
179
|
+
add_command
|
180
|
+
}
|
181
|
+
puts "END"
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
def add_command
|
186
|
+
dsl {
|
187
|
+
puts "command"
|
188
|
+
}
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
test_instance = test_class.new
|
193
|
+
expect(test_instance.to_s).to eql("BEGIN\n command\nEND\n")
|
194
|
+
end
|
195
|
+
|
196
|
+
it "property handles plain #puts calls within a called method nested within DSL" do
|
197
|
+
test_class = Class.new(subject.class) do
|
198
|
+
def initialize
|
199
|
+
super
|
200
|
+
|
201
|
+
dsl {
|
202
|
+
puts "BEGIN"
|
203
|
+
indented {add_command}
|
204
|
+
puts "END"
|
205
|
+
}
|
206
|
+
end
|
207
|
+
|
208
|
+
def add_command
|
209
|
+
puts "command"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
test_instance = test_class.new
|
214
|
+
expect(test_instance.to_s).to eql("BEGIN\n command\nEND\n")
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|