pg_query 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/LICENSE +7 -0
- data/Rakefile +14 -0
- data/ext/pg_query/extconf.rb +35 -0
- data/ext/pg_query/pg_query.c +107 -0
- data/ext/pg_query/pg_query.sym +1 -0
- data/lib/pg_query/parse.rb +181 -0
- data/lib/pg_query/parse_error.rb +9 -0
- data/lib/pg_query/version.rb +3 -0
- data/lib/pg_query.rb +5 -0
- metadata +95 -0
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
|
data/lib/pg_query.rb
ADDED
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: []
|