├── extension ├── .gitignore ├── Makefile ├── annotate_query.sql.in └── annotate_query.c ├── example ├── query.cmd ├── query.in └── query.out ├── LICENSE ├── queryparser.c └── README.md /extension/.gitignore: -------------------------------------------------------------------------------- 1 | *.sql 2 | *.o 3 | *.so 4 | -------------------------------------------------------------------------------- /example/query.cmd: -------------------------------------------------------------------------------- 1 | cat query | sed s/'?'/placeholder/g | sed ':a;N;$!ba;s/\n/ /g' | /tmp/queryparser 2 | -------------------------------------------------------------------------------- /extension/Makefile: -------------------------------------------------------------------------------- 1 | MODULES = annotate_query 2 | DATA_built = annotate_query.sql 3 | DATA = uninstall_annotate_query.sql 4 | 5 | PG_CONFIG = pg_config 6 | PGXS := $(shell $(PG_CONFIG) --pgxs) 7 | include $(PGXS) 8 | -------------------------------------------------------------------------------- /extension/annotate_query.sql.in: -------------------------------------------------------------------------------- 1 | SET search_path = public; 2 | 3 | CREATE OR REPLACE FUNCTION annotate_query(query text, pretty_print bool DEFAULT false) 4 | RETURNS text 5 | AS 'MODULE_PATHNAME','annotate_query' 6 | LANGUAGE C IMMUTABLE STRICT; 7 | -------------------------------------------------------------------------------- /example/query.in: -------------------------------------------------------------------------------- 1 | SELECT 2 | queries.classification, date_part(?, s.collected_at) AS collected_at, SUM(total_time) / SUM(calls) AS avg 3 | FROM 4 | snapshots s JOIN query_snapshots 5 | ON 6 | (snapshot_id = s.id) JOIN queries 7 | ON 8 | (query_snapshots.query_id = queries.id) 9 | WHERE 10 | s.database_id = ? AND s.collected_at BETWEEN ? AND ? 11 | GROUP 12 | BY queries.classification, collected_at 13 | ORDER 14 | BY collected_at 15 | -------------------------------------------------------------------------------- /extension/annotate_query.c: -------------------------------------------------------------------------------- 1 | #include "postgres.h" 2 | #include "fmgr.h" 3 | #include "utils/builtins.h" 4 | #include "parser/parser.h" 5 | #include "nodes/print.h" 6 | 7 | PG_MODULE_MAGIC; 8 | 9 | 10 | PG_FUNCTION_INFO_V1(annotate_query); 11 | 12 | Datum 13 | annotate_query(PG_FUNCTION_ARGS) 14 | { 15 | text *sql_t = PG_GETARG_TEXT_P(0); 16 | bool pretty_b = PG_GETARG_BOOL(1); 17 | text *out_t; 18 | char *sql, *out; 19 | List *tree; 20 | 21 | /* FIXME: Syntax error handling? */ 22 | 23 | sql = text_to_cstring(sql_t); 24 | tree = raw_parser(sql); 25 | 26 | out = nodeToJSONString(tree); 27 | 28 | out_t = cstring_to_text(out); 29 | PG_RETURN_TEXT_P(out_t); 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, pganalyze Team 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of pganalyze nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without specific 16 | prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /queryparser.c: -------------------------------------------------------------------------------- 1 | #include "postgres.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "utils/memutils.h" 10 | 11 | #include "parser/parser.h" 12 | #include "nodes/print.h" 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | const char* progname = "queryparser"; 19 | bool do_parse(const char* query, char* (*output_fnc)(const void*) ); 20 | 21 | bool do_parse(const char* query, char* (*output_fnc)(const void*) ) 22 | { 23 | MemoryContext ctx = NULL; 24 | List *tree; 25 | 26 | ctx = AllocSetContextCreate(TopMemoryContext, 27 | "RootContext", 28 | ALLOCSET_DEFAULT_MINSIZE, 29 | ALLOCSET_DEFAULT_INITSIZE, 30 | ALLOCSET_DEFAULT_MAXSIZE); 31 | MemoryContextSwitchTo(ctx); 32 | 33 | tree = raw_parser(query); 34 | 35 | if (tree != NULL) 36 | { 37 | char *s; 38 | s = output_fnc(tree); 39 | 40 | printf("%s\n", s); 41 | 42 | pfree(s); 43 | } 44 | 45 | MemoryContextSwitchTo(TopMemoryContext); 46 | MemoryContextDelete(ctx); 47 | 48 | return (tree != NULL); 49 | } 50 | 51 | #define BUFSIZE 32768 52 | 53 | int main(int argc, char **argv) 54 | { 55 | char line[BUFSIZE]; 56 | char* p_nl; 57 | MemoryContextInit(); 58 | 59 | if (argc > 1 && 60 | (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) 61 | { 62 | printf("Parse SQL query from stdin\nUSAGE: queryparser\nOPTIONS:\n\t--json: Output in JSON format\n\t--help: Show this help\n"); 63 | return 0; 64 | } 65 | 66 | if (!fgets(line, BUFSIZE, stdin)) 67 | return 2; /* no data read */ 68 | 69 | p_nl = strchr(line, (int) '\n'); 70 | if (p_nl != NULL) { 71 | *(p_nl) = '\0'; 72 | } else { 73 | return 3; /* no newline */ 74 | } 75 | 76 | if (line[0] == '#' || line[0] == '\0') 77 | return 1; 78 | 79 | if (argc > 1 && strcmp(argv[1], "--json") == 0) 80 | { 81 | return do_parse(line, &nodeToJSONString) ? 0 : 1; 82 | } 83 | return do_parse(line, &nodeToString) ? 0 : 1; 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | queryparser 2 | =========== 3 | 4 | Installation 5 | ------------ 6 | 7 | To get things running, run the following in a checkout of this repo: 8 | 9 | ```shell 10 | # Build PostgreSQL 11 | git clone --depth 1 https://github.com/pganalyze/postgres.git postgresql 12 | 13 | cd postgresql 14 | ./configure 15 | make 16 | cd .. 17 | 18 | # Build Queryparser binary 19 | ./build.sh 20 | ``` 21 | 22 | The built queryparser binary is standalone and works without any PostgreSQL libraries existing or server running. 23 | 24 | **Note:** We use a patched version of PostgreSQL, [with improvements to outfuncs.c](https://github.com/pganalyze/postgres/compare/REL9_3_STABLE...pg_query). 25 | 26 | Usage 27 | ----- 28 | 29 | ```shell 30 | # Parse a query into Postgres' internal format 31 | echo 'SELECT 1' | ./queryparser 32 | 33 | # ({SELECT :distinctClause <> :intoClause <> :targetList ({RESTARGET :name <> :indirection <> :val {A_CONST :val 1 :location 7} :location 7}) :fromClause <> :whereClause <> :groupClause <> :havingClause <> :windowClause <> :valuesLists <> :sortClause <> :limitOffset <> :limitCount <> :lockingClause <> :withClause <> :op 0 :all false :larg <> :rarg <>}) 34 | 35 | # Parse a query into JSON 36 | echo 'SELECT 1' | ./queryparser --json 37 | 38 | # [{"SELECT": {"distinctClause": null, "intoClause": null, "targetList": [{"RESTARGET": {"name": null, "indirection": null, "val": {"A_CONST": {"val": 1, "location": 7}}, "location": 7}}], "fromClause": null, "whereClause": null, "groupClause": null, "havingClause": null, "windowClause": null, "valuesLists": null, "sortClause": null, "limitOffset": null, "limitCount": null, "lockingClause": null, "withClause": null, "op": 0, "all": false, "larg": null, "rarg": null}}] 39 | ``` 40 | 41 | Contributors 42 | ------------ 43 | 44 | - [Michael Renner](https://github.com/terrorobe) 45 | - [Christian Hofstaedtler](https://github.com/zeha) 46 | - [Lukas Fittl](mailto:lukas@fittl.com) 47 | - [Phillip Knauss](https://github.com/phillipknauss) 48 | 49 | License 50 | ------- 51 | 52 | Copyright (c) 2014, pganalyze Team
53 | queryparser is licensed under the 3-clause BSD license, see LICENSE file for details. 54 | -------------------------------------------------------------------------------- /example/query.out: -------------------------------------------------------------------------------- 1 | ( 2 | {SELECT 3 | :distinctClause <> 4 | :intoClause <> 5 | :targetList ( 6 | {RESTARGET 7 | :name <> 8 | :indirection <> 9 | :val 10 | {COLUMNREF 11 | :fields ("queries" "classification") 12 | :location 8 13 | } 14 | :location 8 15 | } 16 | {RESTARGET 17 | :name collected_at 18 | :indirection <> 19 | :val 20 | {FUNCCALL 21 | :funcname ("date_part") 22 | :args ( 23 | {COLUMNREF 24 | :fields ("placeholder") 25 | :location 42 26 | } 27 | {COLUMNREF 28 | :fields ("s" "collected_at") 29 | :location 55 30 | } 31 | ) 32 | :agg_order <> 33 | :agg_filter <> 34 | :agg_within_group false 35 | :agg_star false 36 | :agg_distinct false 37 | :func_variadic false 38 | :over <> 39 | :location 32 40 | } 41 | :location 32 42 | } 43 | {RESTARGET 44 | :name avg 45 | :indirection <> 46 | :val 47 | {AEXPR 48 | :name ("/") 49 | :lexpr 50 | {FUNCCALL 51 | :funcname ("sum") 52 | :args ( 53 | {COLUMNREF 54 | :fields ("total_time") 55 | :location 92 56 | } 57 | ) 58 | :agg_order <> 59 | :agg_filter <> 60 | :agg_within_group false 61 | :agg_star false 62 | :agg_distinct false 63 | :func_variadic false 64 | :over <> 65 | :location 88 66 | } 67 | :rexpr 68 | {FUNCCALL 69 | :funcname ("sum") 70 | :args ( 71 | {COLUMNREF 72 | :fields ("calls") 73 | :location 110 74 | } 75 | ) 76 | :agg_order <> 77 | :agg_filter <> 78 | :agg_within_group false 79 | :agg_star false 80 | :agg_distinct false 81 | :func_variadic false 82 | :over <> 83 | :location 106 84 | } 85 | :location 104 86 | } 87 | :location 88 88 | } 89 | ) 90 | :fromClause ( 91 | {JOINEXPR 92 | :jointype 0 93 | :isNatural false 94 | :larg 95 | {JOINEXPR 96 | :jointype 0 97 | :isNatural false 98 | :larg 99 | {RANGEVAR 100 | :schemaname <> 101 | :relname snapshots 102 | :inhOpt 2 103 | :relpersistence p 104 | :alias 105 | {ALIAS 106 | :aliasname s 107 | :colnames <> 108 | } 109 | :location 131 110 | } 111 | :rarg 112 | {RANGEVAR 113 | :schemaname <> 114 | :relname query_snapshots 115 | :inhOpt 2 116 | :relpersistence p 117 | :alias <> 118 | :location 148 119 | } 120 | :usingClause <> 121 | :quals 122 | {AEXPR 123 | :name ("=") 124 | :lexpr 125 | {COLUMNREF 126 | :fields ("snapshot_id") 127 | :location 170 128 | } 129 | :rexpr 130 | {COLUMNREF 131 | :fields ("s" "id") 132 | :location 184 133 | } 134 | :location 182 135 | } 136 | :alias <> 137 | :rtindex 0 138 | } 139 | :rarg 140 | {RANGEVAR 141 | :schemaname <> 142 | :relname queries 143 | :inhOpt 2 144 | :relpersistence p 145 | :alias <> 146 | :location 195 147 | } 148 | :usingClause <> 149 | :quals 150 | {AEXPR 151 | :name ("=") 152 | :lexpr 153 | {COLUMNREF 154 | :fields ("query_snapshots" "query_id") 155 | :location 209 156 | } 157 | :rexpr 158 | {COLUMNREF 159 | :fields ("queries" "id") 160 | :location 236 161 | } 162 | :location 234 163 | } 164 | :alias <> 165 | :rtindex 0 166 | } 167 | ) 168 | :whereClause 169 | {AEXPR AND 170 | :lexpr 171 | {AEXPR 172 | :name ("=") 173 | :lexpr 174 | {COLUMNREF 175 | :fields ("s" "database_id") 176 | :location 256 177 | } 178 | :rexpr 179 | {COLUMNREF 180 | :fields ("placeholder") 181 | :location 272 182 | } 183 | :location 270 184 | } 185 | :rexpr 186 | {AEXPR AND 187 | :lexpr 188 | {AEXPR 189 | :name (">=") 190 | :lexpr 191 | {COLUMNREF 192 | :fields ("s" "collected_at") 193 | :location 288 194 | } 195 | :rexpr 196 | {COLUMNREF 197 | :fields ("placeholder") 198 | :location 311 199 | } 200 | :location 303 201 | } 202 | :rexpr 203 | {AEXPR 204 | :name ("\<=") 205 | :lexpr 206 | {COLUMNREF 207 | :fields ("s" "collected_at") 208 | :location 288 209 | } 210 | :rexpr 211 | {COLUMNREF 212 | :fields ("placeholder") 213 | :location 327 214 | } 215 | :location 303 216 | } 217 | :location 303 218 | } 219 | :location 284 220 | } 221 | :groupClause ( 222 | {COLUMNREF 223 | :fields ("queries" "classification") 224 | :location 350 225 | } 226 | {COLUMNREF 227 | :fields ("collected_at") 228 | :location 374 229 | } 230 | ) 231 | :havingClause <> 232 | :windowClause <> 233 | :valuesLists <> 234 | :sortClause ( 235 | {SORTBY 236 | :node 237 | {COLUMNREF 238 | :fields ("collected_at") 239 | :location 398 240 | } 241 | :sortby_dir 0 242 | :sortby_nulls 0 243 | :useOp <> 244 | :location -1 245 | } 246 | ) 247 | :limitOffset <> 248 | :limitCount <> 249 | :lockingClause <> 250 | :withClause <> 251 | :op 0 252 | :all false 253 | :larg <> 254 | :rarg <> 255 | } 256 | ) 257 | --------------------------------------------------------------------------------