piggly-nsd 2.3.3

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 (96) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +170 -0
  3. data/Rakefile +33 -0
  4. data/bin/piggly +8 -0
  5. data/lib/piggly/command/base.rb +148 -0
  6. data/lib/piggly/command/report.rb +162 -0
  7. data/lib/piggly/command/trace.rb +90 -0
  8. data/lib/piggly/command/untrace.rb +78 -0
  9. data/lib/piggly/command.rb +8 -0
  10. data/lib/piggly/compiler/cache_dir.rb +119 -0
  11. data/lib/piggly/compiler/coverage_report.rb +63 -0
  12. data/lib/piggly/compiler/trace_compiler.rb +117 -0
  13. data/lib/piggly/compiler.rb +7 -0
  14. data/lib/piggly/config.rb +80 -0
  15. data/lib/piggly/dumper/index.rb +121 -0
  16. data/lib/piggly/dumper/qualified_name.rb +36 -0
  17. data/lib/piggly/dumper/qualified_type.rb +141 -0
  18. data/lib/piggly/dumper/reified_procedure.rb +172 -0
  19. data/lib/piggly/dumper/skeleton_procedure.rb +112 -0
  20. data/lib/piggly/dumper.rb +9 -0
  21. data/lib/piggly/installer.rb +137 -0
  22. data/lib/piggly/parser/grammar.tt +748 -0
  23. data/lib/piggly/parser/nodes.rb +378 -0
  24. data/lib/piggly/parser/traversal.rb +50 -0
  25. data/lib/piggly/parser/treetop_ruby19_patch.rb +21 -0
  26. data/lib/piggly/parser.rb +69 -0
  27. data/lib/piggly/profile.rb +108 -0
  28. data/lib/piggly/reporter/base.rb +106 -0
  29. data/lib/piggly/reporter/html_dsl.rb +63 -0
  30. data/lib/piggly/reporter/index.rb +114 -0
  31. data/lib/piggly/reporter/procedure.rb +129 -0
  32. data/lib/piggly/reporter/resources/highlight.js +38 -0
  33. data/lib/piggly/reporter/resources/piggly.css +515 -0
  34. data/lib/piggly/reporter/resources/sortable.js +493 -0
  35. data/lib/piggly/reporter.rb +8 -0
  36. data/lib/piggly/tags.rb +280 -0
  37. data/lib/piggly/task.rb +215 -0
  38. data/lib/piggly/util/blankslate.rb +114 -0
  39. data/lib/piggly/util/cacheable.rb +19 -0
  40. data/lib/piggly/util/enumerable.rb +44 -0
  41. data/lib/piggly/util/file.rb +17 -0
  42. data/lib/piggly/util/process_queue.rb +96 -0
  43. data/lib/piggly/util/thunk.rb +39 -0
  44. data/lib/piggly/util.rb +9 -0
  45. data/lib/piggly/version.rb +15 -0
  46. data/lib/piggly.rb +20 -0
  47. data/spec/examples/compiler/cacheable_spec.rb +190 -0
  48. data/spec/examples/compiler/report_spec.rb +25 -0
  49. data/spec/examples/compiler/trace_spec.rb +123 -0
  50. data/spec/examples/config_spec.rb +63 -0
  51. data/spec/examples/dumper/index_spec.rb +199 -0
  52. data/spec/examples/dumper/procedure_spec.rb +116 -0
  53. data/spec/examples/grammar/expression_spec.rb +302 -0
  54. data/spec/examples/grammar/statements/assignment_spec.rb +70 -0
  55. data/spec/examples/grammar/statements/declaration_spec.rb +21 -0
  56. data/spec/examples/grammar/statements/exception_spec.rb +78 -0
  57. data/spec/examples/grammar/statements/if_spec.rb +191 -0
  58. data/spec/examples/grammar/statements/loop_spec.rb +41 -0
  59. data/spec/examples/grammar/statements/sql_spec.rb +71 -0
  60. data/spec/examples/grammar/tokens/comment_spec.rb +58 -0
  61. data/spec/examples/grammar/tokens/datatype_spec.rb +58 -0
  62. data/spec/examples/grammar/tokens/identifier_spec.rb +74 -0
  63. data/spec/examples/grammar/tokens/keyword_spec.rb +44 -0
  64. data/spec/examples/grammar/tokens/label_spec.rb +40 -0
  65. data/spec/examples/grammar/tokens/literal_spec.rb +30 -0
  66. data/spec/examples/grammar/tokens/lval_spec.rb +50 -0
  67. data/spec/examples/grammar/tokens/number_spec.rb +34 -0
  68. data/spec/examples/grammar/tokens/sqlkeywords_spec.rb +45 -0
  69. data/spec/examples/grammar/tokens/string_spec.rb +54 -0
  70. data/spec/examples/grammar/tokens/whitespace_spec.rb +40 -0
  71. data/spec/examples/installer_spec.rb +59 -0
  72. data/spec/examples/parser/nodes_spec.rb +73 -0
  73. data/spec/examples/parser/traversal_spec.rb +14 -0
  74. data/spec/examples/parser_spec.rb +118 -0
  75. data/spec/examples/profile_spec.rb +153 -0
  76. data/spec/examples/reporter/html/dsl_spec.rb +0 -0
  77. data/spec/examples/reporter/html/index_spec.rb +0 -0
  78. data/spec/examples/reporter/html_spec.rb +1 -0
  79. data/spec/examples/reporter_spec.rb +0 -0
  80. data/spec/examples/tags_spec.rb +285 -0
  81. data/spec/examples/task_spec.rb +0 -0
  82. data/spec/examples/util/cacheable_spec.rb +41 -0
  83. data/spec/examples/util/enumerable_spec.rb +64 -0
  84. data/spec/examples/util/file_spec.rb +40 -0
  85. data/spec/examples/util/process_queue_spec.rb +16 -0
  86. data/spec/examples/util/thunk_spec.rb +59 -0
  87. data/spec/examples/version_spec.rb +0 -0
  88. data/spec/issues/007_spec.rb +25 -0
  89. data/spec/issues/008_spec.rb +73 -0
  90. data/spec/issues/018_spec.rb +25 -0
  91. data/spec/issues/028_spec.rb +48 -0
  92. data/spec/issues/032_spec.rb +98 -0
  93. data/spec/issues/036_spec.rb +41 -0
  94. data/spec/spec_helper.rb +312 -0
  95. data/spec/spec_suite.rb +5 -0
  96. metadata +162 -0
@@ -0,0 +1,8 @@
1
+ module Piggly
2
+ module Command
3
+ autoload :Base, "piggly/command/base"
4
+ autoload :Report, "piggly/command/report"
5
+ autoload :Trace, "piggly/command/trace"
6
+ autoload :Untrace, "piggly/command/untrace"
7
+ end
8
+ end
@@ -0,0 +1,119 @@
1
+ # encoding: utf-8
2
+
3
+ module Piggly
4
+ module Compiler
5
+
6
+ #
7
+ # Each cache unit (any group of data that should be expired and created
8
+ # together) can be broken apart, to prevent unmarshaling a huge block of
9
+ # data all at once.
10
+ #
11
+ # The interface works like a Hash, so the compile method should return a
12
+ # hash of objects. Each object is writen to a different file (named by the
13
+ # hash key) within the same directory. String objects are (usually) read
14
+ # and written directly to disk, while all other objects are (un-)Marshal'd
15
+ #
16
+ # Cache invalidation is done by comparing mtime timestamps on the cached
17
+ # object's file to all the "source" files (ruby libs, input files, etc)
18
+ # required to regenerate the data.
19
+ #
20
+ class CacheDir
21
+ # Non-printable ASCII char indicates data should be Marshal'd
22
+ HINT = /[\u{0000}-\u{0008}\u{000E}-\u{001F}\u{007F}-\u{00C0}]/
23
+
24
+ def initialize(dir)
25
+ @dir = dir
26
+ @data = Hash.new do |h, k|
27
+ path = File.join(@dir, k.to_s)
28
+ if File.exist?(path)
29
+ h[k.to_s] = File.open(path, "rb:UTF-8") do |io|
30
+ # Detect Marshal'd data
31
+ if io.read(2) !~ HINT
32
+ io.rewind
33
+ io.read
34
+ else
35
+ io.rewind
36
+ Marshal.load(io)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ # Load given key from file system into memory if needed
44
+ # @return [Object]
45
+ def [](key)
46
+ @data[key.to_s]
47
+ end
48
+
49
+ # Writes through to file system
50
+ # @return [void]
51
+ def []=(key, value)
52
+ @data[key.to_s] = value
53
+ write(key.to_s => value)
54
+ end
55
+
56
+ # Writes through to file system and returns self
57
+ # @return [CacheDir] self
58
+ def update(hash)
59
+ hash.each{|k,v| self[k] = v }
60
+ self
61
+ end
62
+
63
+ # @return [void]
64
+ def delete(key)
65
+ path = File.join(@dir, key.to_s)
66
+ File.unlink(path) if File.exist?(path)
67
+ @data.delete(key)
68
+ end
69
+
70
+ # @return [Array<String>]
71
+ def keys
72
+ Dir[@dir + "/*"].map{|f| File.basename(f) }
73
+ end
74
+
75
+ # Creates cachedir, destroys its contents, and returns self
76
+ # @return [CacheDir] self
77
+ def clear
78
+ @data.clear
79
+
80
+ if File.exist?(@dir)
81
+ FileUtils.rm(Dir["#{@dir}/*"])
82
+ FileUtils.touch(@dir)
83
+ else
84
+ FileUtils.mkdir(@dir)
85
+ end
86
+
87
+ self
88
+ end
89
+
90
+ # Clears entire cache, replaces contents, and returns self
91
+ # @return [CacheDir] self
92
+ def replace(hash)
93
+ clear
94
+ update(hash)
95
+ end
96
+
97
+ private
98
+
99
+ # Serializes each entry to disk
100
+ # @return [void]
101
+ def write(hash)
102
+ FileUtils.mkdir(@dir) unless File.exist?(@dir)
103
+ FileUtils.touch(@dir) # Update mtime
104
+
105
+ hash.each do |key, value|
106
+ File.open(File.join(@dir, key.to_s), "wb:UTF-8") do |io|
107
+ # Marshal if the first two bytes contain non-ASCII
108
+ if value.is_a?(String) and value[0,2] !~ HINT
109
+ io.write(value)
110
+ else
111
+ Marshal.dump(value, io)
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,63 @@
1
+ module Piggly
2
+ module Compiler
3
+
4
+ #
5
+ # Produces HTML output to report coverage of tagged nodes in the tree
6
+ #
7
+ class CoverageReport
8
+ include Reporter::HtmlDsl
9
+
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ def compile(procedure, profile)
15
+ trace = Compiler::TraceCompiler.new(@config)
16
+
17
+ if trace.stale?(procedure)
18
+ raise StaleCacheError,
19
+ "stale cached syntax tree for #{procedure.name}"
20
+ end
21
+
22
+ # Get (copies of) the tagged nodes from the compiled tree
23
+ data = trace.compile(procedure)
24
+
25
+ return :html => traverse(data[:tree], profile),
26
+ :lines => 1 .. procedure.source(@config).count("\n") + 1
27
+ end
28
+
29
+ protected
30
+
31
+ # @return [String]
32
+ def traverse(node, profile, string = "")
33
+ if node.tagged?
34
+ tag = profile[node.tag_id]
35
+
36
+ if tag.complete?
37
+ string << %[<span class="#{tag.style}" id="T#{tag.id}">]
38
+ else
39
+ string << %[<span class="#{tag.style}" id="T#{tag.id}" title="#{tag.description}">]
40
+ end
41
+ end
42
+
43
+ if node.terminal?
44
+ if style = node.style
45
+ string << %[<span class="#{style}">#{e(node.text_value)}</span>]
46
+ else
47
+ string << e(node.text_value)
48
+ end
49
+ else
50
+ # Non-terminals never write their text_value
51
+ node.elements.each{|child| traverse(child, profile, string) }
52
+ end
53
+
54
+ if node.tagged?
55
+ string << %[</span>]
56
+ end
57
+
58
+ string
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,117 @@
1
+ module Piggly
2
+ module Compiler
3
+
4
+ #
5
+ # Walks the parse tree, attaching Tag values and rewriting source code to ping them.
6
+ #
7
+ class TraceCompiler
8
+ include Util::Cacheable
9
+
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ # Is the cache_path is older than its source path or the other files?
15
+ def stale?(procedure)
16
+ Util::File.stale?(cache_path(procedure.source_path(@config)),
17
+ procedure.source_path(@config),
18
+ *self.class.cache_sources)
19
+ end
20
+
21
+ def compile(procedure)
22
+ cache = CacheDir.new(cache_path(procedure.source_path(@config)))
23
+
24
+ if stale?(procedure)
25
+ begin
26
+ $stdout.puts "Compiling #{procedure.name}"
27
+ tree = Parser.parse(IO.read(procedure.source_path(@config)))
28
+ tree = tree.force! if tree.respond_to?(:thunk?)
29
+
30
+ tags = []
31
+ code = traverse(tree, procedure.oid, tags)
32
+
33
+ cache.replace(:tree => tree, :code => code, :tags => tags)
34
+ rescue RuntimeError => e
35
+ $stdout.puts <<-EXMSG
36
+ ****
37
+ Error compiling procedure #{procedure.name}
38
+ Source: #{procedure.source_path(@config)}
39
+ Exception Message:
40
+ #{e.message}
41
+ ****
42
+ EXMSG
43
+ end
44
+
45
+ end
46
+
47
+ cache
48
+ end
49
+
50
+ protected
51
+
52
+ # Rewrites the parse tree to call instrumentation helpers, and destructively
53
+ # updates `tags` by appending the tags of instrumented nodes
54
+ # @return [String]
55
+ def traverse(node, oid, tags)
56
+ if node.terminal? or node.expression?
57
+ node.source_text
58
+ else
59
+ if node.respond_to?(:condStub) and node.respond_to?(:cond)
60
+ # Preserve opening parenthesis and whitespace before injecting code. This way
61
+ # IF(test) becomes IF(piggly_cond(TAG, test)) instead of IFpiggly_cond(TAG, (test))
62
+ pre, cond = node.cond.expr.text_value.match(/\A(\(?[\t\n\r ]*)(.+)\z/m).captures
63
+ node.cond.source_text = ""
64
+
65
+ tags << node.cond.tag(oid)
66
+
67
+ node.condStub.source_text = "#{pre}public.piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, (#{cond}))"
68
+ node.condStub.source_text << traverse(node.cond.tail, oid, tags) # preserve trailing whitespace
69
+ end
70
+
71
+ if node.respond_to?(:bodyStub)
72
+ if node.respond_to?(:exitStub) and node.respond_to?(:cond)
73
+ tags << node.body.tag(oid)
74
+ tags << node.cond.tag(oid)
75
+
76
+ # Hack to simulate a loop conditional statement in stmtForLoop and stmtLoop.
77
+ # signal condition is true when body is executed, and false when exit stub is reached
78
+ node.bodyStub.source_text = "perform public.piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, true);#{node.indent(:bodySpace)}"
79
+ node.bodyStub.source_text << "perform public.piggly_branch($PIGGLY$#{node.body.tag_id}$PIGGLY$);#{node.indent(:bodySpace)}"
80
+
81
+ if node.respond_to?(:doneStub)
82
+ # Signal the end of an iteration was reached
83
+ node.doneStub.source_text = "#{node.indent(:bodySpace)}perform public.piggly_signal($PIGGLY$#{node.cond.tag_id}$PIGGLY$, $PIGGLY$@$PIGGLY$);"
84
+ node.doneStub.source_text << node.body.indent
85
+ end
86
+
87
+ # Signal the loop terminated
88
+ node.exitStub.source_text = "\n#{node.indent}perform public.piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, false);"
89
+ elsif node.respond_to?(:body)
90
+ # Unconditional branches (or blocks)
91
+ # BEGIN ... END;
92
+ # ... ELSE ... END;
93
+ # CONTINUE label;
94
+ # EXIT label;
95
+ tags << node.body.tag(oid)
96
+ node.bodyStub.source_text = "perform public.piggly_branch($PIGGLY$#{node.body.tag_id}$PIGGLY$);#{node.indent(:bodySpace)}"
97
+ end
98
+ end
99
+
100
+ # Traverse children (in which we just injected code)
101
+ node.elements.map{|e| traverse(e, oid, tags) }.join
102
+ end
103
+ end
104
+ end
105
+
106
+ class << TraceCompiler
107
+
108
+ # Each of these files' mtimes are used to determine when another file is stale
109
+ def cache_sources
110
+ [Parser.grammar_path,
111
+ Parser.parser_path,
112
+ Parser.nodes_path]
113
+ end
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,7 @@
1
+ module Piggly
2
+ module Compiler
3
+ autoload :CacheDir, "piggly/compiler/cache_dir"
4
+ autoload :TraceCompiler, "piggly/compiler/trace_compiler"
5
+ autoload :CoverageReport, "piggly/compiler/coverage_report"
6
+ end
7
+ end
@@ -0,0 +1,80 @@
1
+ module Piggly
2
+ class Config
3
+ end
4
+
5
+ class << Config
6
+ def path(root, file=nil)
7
+ if file.nil?
8
+ root
9
+ else
10
+ file[%r{^\.\.|^\/|^(?:[A-Z]:)?/}i] ?
11
+ file : # ../path, /path, or D:\path that isn't relative to root
12
+ File.join(root, file)
13
+ end
14
+ end
15
+
16
+ def mkpath(root, file=nil)
17
+ if file.nil?
18
+ FileUtils.makedirs(root)
19
+ root
20
+ else
21
+ path = path(root, file)
22
+ FileUtils.makedirs(File.dirname(path))
23
+ path
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def config_accessor(hash)
30
+ hash = hash.clone
31
+ hash.keys.each do |name|
32
+ define_method(name) do
33
+ instance_variable_get("@#{name}") || hash[name]
34
+ end
35
+
36
+ define_method("#{name}?") do
37
+ instance_variable_get("@#{name}") || hash[name]
38
+ end
39
+
40
+ define_method("#{name}=") do |value|
41
+ instance_variable_set("@#{name}", value)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ class Config
48
+ attr_accessor \
49
+ :cache_root,
50
+ :report_root,
51
+ :database_yml,
52
+ :connection_name,
53
+ :trace_prefix,
54
+ :accumulate,
55
+ :dry_run,
56
+ :filters
57
+
58
+ alias accumulate? accumulate
59
+ alias dry_run? dry_run
60
+
61
+ def path(*args)
62
+ self.class.path(*args)
63
+ end
64
+
65
+ def mkpath(*args)
66
+ self.class.mkpath(*args)
67
+ end
68
+
69
+ def initialize
70
+ @cache_root = File.expand_path("#{Dir.pwd}/piggly/cache")
71
+ @report_root = File.expand_path("#{Dir.pwd}/piggly/reports")
72
+ @database_yml = nil
73
+ @connection_name = "piggly"
74
+ @trace_prefix = "PIGGLY"
75
+ @accumulate = false
76
+ @dry_run = false
77
+ @filters = []
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,121 @@
1
+ module Piggly
2
+ module Dumper
3
+
4
+ #
5
+ # The index file stores metadata about every procedure, but the source
6
+ # code is stored in a separate file for each procedure.
7
+ #
8
+ class Index
9
+
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ # @return [String]
15
+ def path
16
+ @config.mkpath("#{@config.cache_root}/Dumper", "index.yml")
17
+ end
18
+
19
+ # Updates the index with the given list of Procedure values
20
+ # @return [void]
21
+ def update(procedures)
22
+ newest = Util::Enumerable.index_by(procedures){|x| x.identifier }
23
+
24
+ removed = index.values.reject{|p| newest.include?(p.identifier) }
25
+ removed.each{|p| p.purge_source(@config) }
26
+
27
+ added = procedures.reject{|p| index.include?(p.identifier) }
28
+ added.each{|p| p.store_source(@config) }
29
+
30
+ changed = procedures.select do |p|
31
+ if mine = index[p.identifier]
32
+ # If both are skeletons, they will have the same source because they
33
+ # are read from the same file, so don't bother checking that case
34
+ not (mine.skeleton? and p.skeleton?) and
35
+ mine.source(@config) != p.source(@config)
36
+ end
37
+ end
38
+ changed.each{|p| p.store_source(@config) }
39
+
40
+ @index = newest
41
+ store_index
42
+ end
43
+
44
+ # Returns a list of Procedure values from the index
45
+ def procedures
46
+ index.values
47
+ end
48
+
49
+ # Returns the Procedure with the given identifier
50
+ def [](identifier)
51
+ p = index[identifier]
52
+ p.dup if p
53
+ end
54
+
55
+ # Returns the shortest human-readable label that distinctly identifies
56
+ # the given procedure from the other procedures in the index
57
+ def label(procedure)
58
+ others =
59
+ procedures.reject{|p| p.oid == procedure.oid }
60
+
61
+ same =
62
+ others.all?{|p| p.name.schema == procedure.name.schema }
63
+
64
+ name =
65
+ if same
66
+ procedure.name.name
67
+ else
68
+ procedure.name.to_s
69
+ end
70
+
71
+ samenames =
72
+ others.select{|p| p.name == procedure.name }
73
+
74
+ if samenames.empty?
75
+ # Name is unique enough
76
+ name.to_s
77
+ else
78
+ sameargs =
79
+ samenames.select{|p| p.arg_types == procedure.arg_types }
80
+
81
+ if sameargs.empty?
82
+ # Name and arg types are unique enough
83
+ "#{name}(#{procedure.arg_types.join(", ")})"
84
+ else
85
+ samemodes =
86
+ sameargs.select{|p| p.arg_modes == procedure.arg_modes }
87
+
88
+ if samemodes.empty?
89
+ # Name, arg types, and arg modes are unique enough
90
+ "#{name}(#{procedure.arg_modes.zip(procedure.arg_types).map{|a,b| "#{a} #{b}" }.join(", ")})"
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def index
99
+ @index ||= load_index
100
+ end
101
+
102
+ # Load the index from disk
103
+ def load_index
104
+ contents =
105
+ unless File.exist?(path)
106
+ []
107
+ else
108
+ YAML.unsafe_load(File.read(path, encoding: 'UTF-8'))
109
+ end
110
+
111
+ Util::Enumerable.index_by(contents){|x| x.identifier }
112
+ end
113
+
114
+ # Write the index to disk
115
+ def store_index
116
+ File.open(path, "wb:UTF-8"){|io| YAML.dump(procedures.map{|p| p.skeleton }, io) }
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,36 @@
1
+ module Piggly
2
+ module Dumper
3
+
4
+ class QualifiedName
5
+ attr_reader :schema, :name
6
+
7
+ def initialize(schema, name)
8
+ @schema, @name = schema, name
9
+ end
10
+
11
+ # @return [String]
12
+ def quote
13
+ if @schema
14
+ '"' + @schema + '"."' + @name + '"'
15
+ else
16
+ '"' + @name + '"'
17
+ end
18
+ end
19
+
20
+ # @return [String]
21
+ def to_s
22
+ if @schema
23
+ @schema + "." + @name
24
+ else
25
+ @name
26
+ end
27
+ end
28
+
29
+ # @return [Boolean]
30
+ def ==(other)
31
+ self.to_s == other.to_s
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,141 @@
1
+ module Piggly
2
+ module Dumper
3
+
4
+ # used for RETURN TABLE(...)
5
+ class RecordType
6
+ attr_reader :types, :names, :modes, :defaults
7
+
8
+ def initialize(types, names, modes, defaults)
9
+ @types, @names, @modes, @defaults =
10
+ types, names, modes, defaults
11
+ end
12
+
13
+ def quote
14
+ "table (#{@types.zip(@names, @modes, @defaults).map do |type, name, mode, default|
15
+ "#{name.quote + " " if name}#{type.quote}#{" default " + default if default}"
16
+ end.join(", ")})"
17
+ end
18
+
19
+ def table?
20
+ true
21
+ end
22
+ end
23
+
24
+ class QualifiedType
25
+ attr_reader :schema, :name
26
+
27
+ def self.parse(name, rest = nil)
28
+ if rest.to_s == ""
29
+ schema = nil
30
+ else
31
+ schema = unquote(name)
32
+ name = rest
33
+ end
34
+
35
+ case name
36
+ when /(.*)\[\]$/
37
+ name = $1
38
+ array = "[]"
39
+ else
40
+ array = ""
41
+ end
42
+
43
+ if schema.to_s == ""
44
+ fst, snd = name.split(".", 2)
45
+ if snd.nil?
46
+ new(nil, unquote(fst), array)
47
+ else
48
+ new(unquote(fst), unquote(snd), array)
49
+ end
50
+ else
51
+ new(schema, unquote(name), array)
52
+ end
53
+ end
54
+
55
+ def self.unquote(s)
56
+ return s if s.nil?
57
+ s[/^"(.*)"$/, 1] || s
58
+ end
59
+
60
+ def initialize(schema, name, array)
61
+ @schema, @name, @array = schema, name, array
62
+ end
63
+
64
+ def table?
65
+ false
66
+ end
67
+
68
+ def shorten
69
+ self.class.new(nil, @name, @array)
70
+ end
71
+
72
+ def quote
73
+ if @schema
74
+ '"' + @schema + '"."' + normalize(@name) + '"' + @array
75
+ else
76
+ '"' + normalize(@name) + '"' + @array
77
+ end
78
+ end
79
+
80
+ def to_s
81
+ unless [nil, "", "pg_catalog"].include?(@schema)
82
+ @schema + "." + readable(@name) + @array
83
+ else
84
+ readable(@name) + @array
85
+ end
86
+ end
87
+
88
+ def ==(other)
89
+ self.to_s == other.to_s
90
+ end
91
+
92
+ protected
93
+
94
+ def normalize(name)
95
+ unless [nil, "", "pg_catalog"].include?(@schema)
96
+ return name
97
+ end
98
+
99
+ # select format_type(ret.oid, null), ret.typname
100
+ # from pg_type as ret
101
+ # where ret.typname <> format_type(ret.oid, null)
102
+ # and ret.typname not like '\\_%'
103
+ # group by ret.typname, format_type(ret.oid, null)
104
+ # order by format_type(ret.oid, null);
105
+ case name
106
+ when '"any"' then "any"
107
+ when "bigint" then "int8"
108
+ when "bit varying" then "varbit"
109
+ when "boolean" then "bool"
110
+ when '"char"' then "char"
111
+ when "character" then "bpchar"
112
+ when "character varying" then "varchar"
113
+ when "double precision" then "float8"
114
+ when "information_schema\.(.*)" then $1
115
+ when "integer" then "int4"
116
+ when "real" then "float4"
117
+ when "smallint" then "int2"
118
+ when "timestamp without time zone" then "timestamp"
119
+ when "timestamp with time zone" then "timestamptz"
120
+ when "time without time zone" then "time"
121
+ when "time with time zone" then "timetz"
122
+ else name
123
+ end
124
+ end
125
+
126
+ def readable(name)
127
+ case name
128
+ when /^_(.*)/ then "#{readable($1)}[]"
129
+ when "bpchar" then "char"
130
+ when /^float4(.*)/ then "real#{$1}"
131
+ when /^int2(.*)/ then "smallint#{$1}"
132
+ when /^int4(.*)/ then "int#{$1}"
133
+ when /^int8(.*)/ then "bigint#{$1}"
134
+ when /^serial4(.*)/ then "serial#{$1}"
135
+ else name
136
+ end
137
+ end
138
+ end
139
+
140
+ end
141
+ end