pg_query 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZTM4ODg0YjI2Mzk1MmZhODhiOGQwNDJlZDNhMGJiZTViYjRlNzNmMQ==
5
+ data.tar.gz: !binary |-
6
+ NjUxZDg4NGIxY2NkN2U2ZDczMTA3ZjRmMzIxYmEwNWI5ZGE5OGIwOQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NWY3NGEyMmNlNDJjMGM3NDU2YTE3ZDVmYTdiYjNiYmU3OTYxNmQxYThiMTEy
10
+ NzBlMTljZjE1NmU3OGIwM2FiZmRjMmQ1MjQ0ZDJkMDVkZTk2ZWJhMzgzNTBm
11
+ NGI0NjUyNDM3ZWY3OTNjM2Y3NzVmNTBkOWZhZmU2MzM1MTAxODg=
12
+ data.tar.gz: !binary |-
13
+ Nzc3YTAxODJiYjdhMGEwMTUyNmZlMmUyNDRkNWEwZjkzODI0Y2ZiYWJmMzFm
14
+ NWM3YzAyMTFkOTU4YmVjYzllMGU2ODA3Y2YzZWQ2MGIxOTNhNGJmMDI1Yjlk
15
+ MWFmNWM1NGE2Njk1MWQzNGE0ZjI0MDYyZTc5NTMzMTI4YjA0ZWM=
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2014, pganalyze
2
+
3
+ Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies.
4
+
5
+ IN NO EVENT SHALL pganalyze BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF pganalyze HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6
+
7
+ pganalyze SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND pganalyze HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/extensiontask"
3
+ require "rspec/core/rake_task"
4
+
5
+ Rake::ExtensionTask.new "pg_query" do |ext|
6
+ ext.lib_dir = "lib/pg_query"
7
+ end
8
+
9
+ RSpec::Core::RakeTask.new
10
+
11
+ task spec: :compile
12
+
13
+ task default: :spec
14
+ task test: :spec
@@ -0,0 +1,35 @@
1
+ require 'mkmf'
2
+
3
+ workdir = Dir.pwd
4
+ pgdir = File.join(workdir, "postgres")
5
+
6
+ # Download & compile PostgreSQL if we don't have it yet
7
+ #
8
+ # Note: We intentionally use a patched version that fixes bugs in outfuncs.c
9
+ if !Dir.exists?(pgdir)
10
+ unless File.exists?("#{workdir}/postgres.zip")
11
+ system("curl https://codeload.github.com/pganalyze/postgres/zip/more-outfuncs -o #{workdir}/postgres.zip") || raise("ERROR")
12
+ end
13
+ system("unzip -q #{workdir}/postgres.zip -d #{workdir}") || raise("ERROR")
14
+ system("mv #{workdir}/postgres-more-outfuncs #{pgdir}") || raise("ERROR")
15
+ system("cd #{pgdir}; ./configure") || raise("ERROR")
16
+ system("cd #{pgdir}; make") || raise("ERROR")
17
+ end
18
+
19
+ $objs = `find #{pgdir}/src/backend -name '*.o' | egrep -v '(main/main\.o|snowball|libpqwalreceiver|conversion_procs)' | xargs echo`
20
+ $objs += " #{pgdir}/src/timezone/localtime.o #{pgdir}/src/timezone/strftime.o #{pgdir}/src/timezone/pgtz.o"
21
+ $objs += " #{pgdir}/src/common/libpgcommon_srv.a #{pgdir}/src/port/libpgport_srv.a"
22
+ $objs = $objs.split(" ")
23
+
24
+ $objs << File.join(File.dirname(__FILE__), "pg_query.o")
25
+
26
+ $CFLAGS << " -I #{pgdir}/src/include"
27
+
28
+ SYMFILE = File.join(File.dirname(__FILE__), "pg_query.sym")
29
+ if RUBY_PLATFORM =~ /darwin/
30
+ $DLDFLAGS << "-exported_symbols_list #{SYMFILE}"
31
+ else
32
+ $DLDFLAGS << "-export-symbols #{SYMFILE}"
33
+ end
34
+
35
+ create_makefile 'pg_query/pg_query'
@@ -0,0 +1,107 @@
1
+ #include "postgres.h"
2
+ #include "utils/memutils.h"
3
+ #include "parser/parser.h"
4
+ #include "nodes/print.h"
5
+
6
+ #include <unistd.h>
7
+ #include <fcntl.h>
8
+
9
+ #include <ruby.h>
10
+
11
+ const char* progname = "pg_query";
12
+
13
+ static void raise_parse_error(ErrorData* error)
14
+ {
15
+ VALUE cPgQuery, cParseError;
16
+ VALUE exc, args[2];
17
+
18
+ cPgQuery = rb_const_get(rb_cObject, rb_intern("PgQuery"));
19
+ cParseError = rb_const_get_at(cPgQuery, rb_intern("ParseError"));
20
+
21
+ args[0] = rb_tainted_str_new_cstr(error->message);
22
+ args[1] = INT2NUM(error->cursorpos);
23
+
24
+ exc = rb_class_new_instance(2, args, cParseError);
25
+
26
+ rb_exc_raise(exc);
27
+ }
28
+
29
+ #define STDERR_BUFFER_LEN 4096
30
+
31
+ static VALUE pg_query_raw_parse(VALUE self, VALUE input)
32
+ {
33
+ Check_Type(input, T_STRING);
34
+
35
+ MemoryContext ctx = NULL;
36
+ VALUE result;
37
+ ErrorData* error = NULL;
38
+ char stderr_buffer[STDERR_BUFFER_LEN + 1] = {0};
39
+ int stderr_global;
40
+ int stderr_pipe[2];
41
+
42
+ ctx = AllocSetContextCreate(TopMemoryContext,
43
+ "RootContext",
44
+ ALLOCSET_DEFAULT_MINSIZE,
45
+ ALLOCSET_DEFAULT_INITSIZE,
46
+ ALLOCSET_DEFAULT_MAXSIZE);
47
+ MemoryContextSwitchTo(ctx);
48
+
49
+ // Setup pipe for stderr redirection
50
+ if (pipe(stderr_pipe) != 0)
51
+ rb_raise(rb_eRuntimeError, "PgQuery._raw_parse: Could not allocate pipe for stderr redirection");
52
+
53
+ fcntl(stderr_pipe[0], F_SETFL, fcntl(stderr_pipe[0], F_GETFL) | O_NONBLOCK);
54
+
55
+ // Redirect stderr to the pipe
56
+ stderr_global = dup(STDERR_FILENO);
57
+ dup2(stderr_pipe[1], STDERR_FILENO);
58
+ close(stderr_pipe[1]);
59
+
60
+ // Parse it!
61
+ PG_TRY();
62
+ {
63
+ List *tree;
64
+ char *str;
65
+
66
+ str = StringValueCStr(input);
67
+ tree = raw_parser(str);
68
+
69
+ str = nodeToString(tree);
70
+
71
+ // Save stderr for result
72
+ read(stderr_pipe[0], stderr_buffer, STDERR_BUFFER_LEN);
73
+
74
+ result = rb_ary_new();
75
+ rb_ary_push(result, rb_tainted_str_new_cstr(str));
76
+ rb_ary_push(result, rb_str_new2(stderr_buffer));
77
+
78
+ pfree(str);
79
+ }
80
+ PG_CATCH();
81
+ {
82
+ error = CopyErrorData();
83
+ FlushErrorState();
84
+ }
85
+ PG_END_TRY();
86
+
87
+ // Restore stderr & return to previous PostgreSQL memory context
88
+ dup2(stderr_global, STDERR_FILENO);
89
+ MemoryContextSwitchTo(TopMemoryContext);
90
+ MemoryContextDelete(ctx);
91
+
92
+ // If we got an error, throw a ParseError exception
93
+ if (error) raise_parse_error(error);
94
+
95
+ return result;
96
+ }
97
+
98
+ void Init_pg_query(void)
99
+ {
100
+ VALUE cPgQuery;
101
+
102
+ MemoryContextInit();
103
+
104
+ cPgQuery = rb_const_get(rb_cObject, rb_intern("PgQuery"));
105
+
106
+ rb_define_singleton_method(cPgQuery, "_raw_parse", pg_query_raw_parse, 1);
107
+ }
@@ -0,0 +1 @@
1
+ _Init_pg_query
@@ -0,0 +1,181 @@
1
+ require 'json'
2
+
3
+ class PgQuery
4
+ def self.parse(query)
5
+ parsetree, stderr = _raw_parse(query)
6
+
7
+ parsetree = [] if parsetree == '<>'
8
+
9
+ if !parsetree.nil? && !parsetree.empty?
10
+ parsetree = parsetree_to_json(parsetree)
11
+ parsetree = JSON.parse(parsetree, max_nesting: 1000)
12
+ end
13
+
14
+ warnings = []
15
+ stderr.each_line do |line|
16
+ next unless line[/^WARNING/]
17
+ warnings << line.strip
18
+ end
19
+
20
+ PgQuery.new(query, parsetree, warnings)
21
+ end
22
+
23
+ attr_reader :query
24
+ attr_reader :parsetree
25
+ attr_reader :warnings
26
+ def initialize(query, parsetree, warnings = [])
27
+ @query = query
28
+ @parsetree = parsetree
29
+ @warnings = warnings
30
+ end
31
+
32
+ protected
33
+ def self.parsetree_to_json(str)
34
+ str.strip!
35
+ control_chars = '(){}: '
36
+ location = nil # :hashname, :key, :value
37
+ structure_stack = [] # :hash, :array (when delimiter opens we push, when delimiter closes we pop)
38
+ open_string = false
39
+ escaped_string = false
40
+ next_char_is_escaped = false
41
+ double_hash_close_in = 0 # This is used to ask for an additional closing delimiter (added when x = 1 and we reach a closing delimiter)
42
+ out = ""
43
+
44
+ i = 0
45
+ loop do
46
+ break if i > str.size-1
47
+
48
+ c = str[i]
49
+ last_location = location
50
+ char_is_escaped = next_char_is_escaped
51
+ next_char_is_escaped = false
52
+ if control_chars.include?(c) && !char_is_escaped && (!open_string || !escaped_string)
53
+ # Space is not always a control character, skip in those cases
54
+ if c == ' ' && last_location == :hashname && str[i+1] != ':'
55
+ out += c
56
+ i += 1
57
+ next
58
+ end
59
+
60
+ # Keep empty nodes as empty hashes (e.g. nodes that can't be output)
61
+ if c == '{' && str[i+1] == '}'
62
+ out += "{}"
63
+ out += ", " if i+2 < str.size && !'})'.include?(str[i+2])
64
+ i += 2 # Skip {}
65
+ next
66
+ end
67
+
68
+ location = nil # All control characters reset location
69
+
70
+ # This should never happen, but if it does, catch it
71
+ if open_string
72
+ out += '"' unless escaped_string
73
+ open_string = false
74
+ escaped_string = false
75
+ end
76
+
77
+ # Write out JSON control characters
78
+ if last_location == :hashname
79
+ out += ': {'
80
+ elsif last_location == :key
81
+ out += ': '
82
+ end
83
+
84
+ case c
85
+ when '('
86
+ out += '['
87
+ when ')'
88
+ out += ']'
89
+ when '{'
90
+ out += '{'
91
+ when '}'
92
+ out += '}}'
93
+ when ':'
94
+ # No JSON equivalent
95
+ end
96
+
97
+ # Handle double hash closes (required for out-of-place nodes like ANY)
98
+ case c
99
+ when '{'
100
+ if double_hash_close_in > 0
101
+ double_hash_close_in += 1
102
+ end
103
+ when '}'
104
+ if double_hash_close_in == 1
105
+ out += '}}'
106
+ double_hash_close_in = 0
107
+ elsif double_hash_close_in > 1
108
+ double_hash_close_in -= 1
109
+ end
110
+ end
111
+
112
+ # Write out delimiter if needed
113
+ if (last_location == :value || '})'.include?(c)) && i+1 < str.size && !'})'.include?(str[i+1])
114
+ out += ', '
115
+ end
116
+
117
+ # Determine new location
118
+ case c
119
+ when '{'
120
+ structure_stack << :hash
121
+ location = :hashname
122
+ when '('
123
+ structure_stack << :array
124
+ location = :value if !control_chars.include?(str[i+1])
125
+ when '}', ')'
126
+ structure_stack.pop
127
+ when ':'
128
+ location = :key
129
+ when ' '
130
+ location = :value if [:value, :key].include?(last_location) && !control_chars.include?(str[i+1])
131
+ end
132
+ else
133
+ if char_is_escaped
134
+ case c
135
+ when '"'
136
+ out += "\\\""
137
+ when '\\'
138
+ out += "\\\\"
139
+ else
140
+ out += c
141
+ end
142
+ elsif str[i] == '<' && str[i+1] == '>' && control_chars.include?(str[i+2])
143
+ # Make <> into null values
144
+ i += 1
145
+ out += "null"
146
+ elsif c == '\\'
147
+ next_char_is_escaped = true # For next round
148
+ # Ignore all other cases
149
+ elsif c[/[A-Z]/] && !open_string && last_location != :value && last_location != :hashname && structure_stack.last == :hash
150
+ # We were not expecting a node name here, but this can happen (e.g. with ANY), try to construct into valid expression
151
+ location = :hashname
152
+ double_hash_close_in = 1
153
+ open_string = true
154
+ out += '"lexpr": {"'
155
+ out += c
156
+ elsif control_chars.include?(str[i-1]) && !open_string && !'0123456789'.include?(c)
157
+ open_string = true
158
+ escaped_string = true if c == '"'
159
+ out += '"' unless escaped_string
160
+ out += c
161
+ c = nil # To avoid close string taking care of us...
162
+ else
163
+ out += c
164
+ end
165
+
166
+ # Close string if next element is control character
167
+ if open_string && !char_is_escaped && c != '\\' &&
168
+ ((last_location == :hashname && ((str[i+1] == '}') || (str[i+1] == ' ' && str[i+2] == ':') || (str[i+1] == ' ' && str[i+2] == ' ' && str[i+3] == ':'))) ||
169
+ (last_location != :hashname && !escaped_string && control_chars.include?(str[i+1])) ||
170
+ (escaped_string && c == '"'))
171
+ out += '"' unless escaped_string
172
+ open_string = false
173
+ escaped_string = false
174
+ end
175
+ end
176
+
177
+ i += 1
178
+ end
179
+ out
180
+ end
181
+ end
@@ -0,0 +1,9 @@
1
+ class PgQuery
2
+ class ParseError < ArgumentError
3
+ attr_reader :location
4
+ def initialize(message, location)
5
+ super(message)
6
+ @location = location
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ class PgQuery
2
+ VERSION = '0.0.2'
3
+ end
data/lib/pg_query.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'pg_query/version'
2
+ require 'pg_query/parse_error'
3
+
4
+ require 'pg_query/pg_query'
5
+ require 'pg_query/parse'
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg_query
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Lukas Fittl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake-compiler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: json
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
55
+ description: Parses SQL queries using a copy of the PostgreSQL server query parser
56
+ email: lukas@fittl.com
57
+ executables: []
58
+ extensions:
59
+ - ext/pg_query/extconf.rb
60
+ extra_rdoc_files: []
61
+ files:
62
+ - LICENSE
63
+ - Rakefile
64
+ - ext/pg_query/extconf.rb
65
+ - ext/pg_query/pg_query.c
66
+ - ext/pg_query/pg_query.sym
67
+ - lib/pg_query.rb
68
+ - lib/pg_query/parse.rb
69
+ - lib/pg_query/parse_error.rb
70
+ - lib/pg_query/version.rb
71
+ homepage: http://github.com/pganalyze/pg_query
72
+ licenses:
73
+ - PostgreSQL
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.2.2
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: PostgreSQL query parsing and normalization library
95
+ test_files: []