diff --git a/.gitignore b/.gitignore index b32b222..c4700b5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ jsquery_gram.h regression.diffs regression.out results +*sw? diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2d16d6a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +before_script: + - sudo su $USER -c ".travis/install" + +script: sudo su $USER -c ".travis/test" + +addons: + postgresql: "9.3" + diff --git a/.travis/install b/.travis/install new file mode 100755 index 0000000..ebc252f --- /dev/null +++ b/.travis/install @@ -0,0 +1,47 @@ +#!/bin/bash + +export ROOT=`pwd` +export BUILD_DIR=/home/travis/build/pg +export SOURCE_DIR=$BUILD_DIR/src + +export PGDATA=$BUILD_DIR/data +export PGPORT=5777 +export PGHOST=localhost + +export PG_BIN=$BUILD_DIR/bin +export PG_CONFIG=$BUILD_DIR + +export PATH=$PATH:$PG_BIN + +sudo apt-get -y -qq --purge remove postgresql libpq-dev libpq5 postgresql-client-common postgresql-common +sudo apt-get -qq update +sudo apt-get -qqy install \ + git \ + build-essential \ + gettext \ + libreadline6 \ + libreadline6-dev \ + zlib1g-dev \ + flex \ + bison \ + libxml2-dev \ + libxslt-dev + +git clone -b REL9_4_STABLE --depth=1 git://git.postgresql.org/git/postgresql.git $SOURCE_DIR + +# XML2_CONFIG=`which xml2-config` cd $SOURCE_DIR ./configure --prefix=$BUILD_DIR --with-libxml &&\ + +cd $SOURCE_DIR && ./configure --prefix=$BUILD_DIR && make && make install + + +cd $ROOT && make USE_PGXS=1 && make install USE_PGXS=1 + +mkdir -p $PGDATA + +$PG_BIN/initdb -D $PGDATA -E utf8 + +# echo "host all all 0.0.0.0/0 md5" >> $PGDATA/pg_hba.conf +# echo "listen_addresses='*'" >> $PGDATA/postgresql.conf +# echo "port=$PGPORT" >> $PGDATA/postgresql.conf + +$PG_BIN/pg_ctl -D $PGDATA start diff --git a/.travis/test b/.travis/test new file mode 100755 index 0000000..0dd1289 --- /dev/null +++ b/.travis/test @@ -0,0 +1,17 @@ +#!/bin/bash + +export BUILD_DIR=/home/travis/build/pg +export SOURCE_DIR=$BUILD_DIR/src + +export PGDATA=$BUILD_DIR/data +export PGPORT=5777 +export PGHOST=localhost + +export PG_BIN=$BUILD_DIR/bin +export PG_CONFIG=$BUILD_DIR + +export PATH=$PATH:$PG_BIN + +export ROOT=`pwd` + +make installcheck USE_PGXS=1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4c90d3b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +FROM ubuntu:14.04 +MAINTAINER Nikolay Ryzhikov + +RUN apt-get -qq update +RUN apt-get -qqy install git \ + build-essential \ + gettext \ + libreadline6 \ + libreadline6-dev \ + zlib1g-dev \ + flex \ + bison \ + libxml2-dev \ + libxslt-dev + +RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen + +RUN useradd -m -s /bin/bash db && echo "db:db"|chpasswd && adduser db sudo +RUN echo 'db ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers + +USER db +ENV HOME /home/db +ENV PG_BRANCH REL9_4_STABLE +ENV PG_REPO git://git.postgresql.org/git/postgresql.git + +RUN git clone -b $PG_BRANCH --depth=1 $PG_REPO $HOME/src +RUN cd $HOME/src && ./configure --prefix=$HOME/bin && make && make install + +ENV SOURCE_DIR $HOME/src + +ENV PATH $HOME/bin/bin:$PATH +ENV PGDATA $HOME/data +ENV PGPORT 5432 +ENV PGHOST localhost +RUN mkdir -p $PGDATA +RUN initdb -D $PGDATA -E utf8 + +RUN echo "host all all 0.0.0.0/0 md5" >> $PGDATA/pg_hba.conf +RUN echo "listen_addresses='*'" >> $PGDATA/postgresql.conf +RUN echo "port=$PGPORT" >> $PGDATA/postgresql.conf + +RUN pg_ctl -D $HOME/data -w start && psql postgres -c "alter user db with password 'db';" + +RUN pg_ctl -D $HOME/data -w start && \ + cd $SOURCE_DIR/contrib/pgcrypto && \ + make && make install && make installcheck && \ + pg_ctl -w stop + +RUN pg_ctl -D $HOME/data -w start && \ + cd $SOURCE_DIR/contrib && \ + git clone https://github.com/akorotkov/jsquery.git && \ + cd jsquery && make && make install && make installcheck && \ + pg_ctl -w stop + +EXPOSE 5432 +CMD pg_ctl -D $HOME/data -w start && psql postgres diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7bac6a --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# jsquery + +[![Build Status](https://travis-ci.org/niquola/jsquery.svg)](https://travis-ci.org/niquola/jsquery) + + +`Jsquery` is PostgreSQL extension, +which provides advanced query language for jsonb documents. + +Features: + +* recursive structure of query +* search in array +* rich set of comparison operators +* types support +* schema support (constraints on keys, values) +* indexes support +* hinting support + + +Jsquery implemented as datatype `jsquery` and operator `@@`. + +Examples: + +`#` - any element array + +```SQL +SELECT '{"a": {"b": [1,2,3]}}'::jsonb @@ 'a.b.# = 2'; +``` + +`%` - any key + +```SQL +SELECT '{"a": {"b": [1,2,3]}}'::jsonb @@ '%.b.# = 2'; +``` + +`*` - anything + +```SQL +SELECT '{"a": {"b": [1,2,3]}}'::jsonb @@ '*.# = 2'; +``` + +`$` - current element + +```SQL +select '{"a": {"b": [1,2,3]}}'::jsonb @@ 'a.b.# ($ = 2 OR $ < 3)'; +``` + +Use "double quotes" for key ! + +```SQL +select 'a1."12222" < 111'::jsquery; +``` + +## Documentation + +* [Syntax](doc/intro.md) +* [Indexes](doc/indexes.md) +* [Optimizer](doc/optimiser.md) + +## Installation + +Requirements: + +* PostgreSQL >= 9.4 + +### Build from sources + +```sh +git clone https://github.com/akorotkov/jsquery.git $SOURCE_DIR/contrib +cd $SOURCE_DIR/contrib/jsquery && make && make install && make installcheck + +# or not from pg sources + +git clone https://github.com/akorotkov/jsquery.git +cd jsquery && make USE_PGXS=1 && make install USE_PGXS=1 + +``` + +### Using docker + +TODO + +## Roadmap + +* Implement jsquery as extension of SQL syntax + +## Contribution + +You can contribute by: + +* stars +* [issues](https://github.com/akorotkov/jsquery/issues) +* documentation +* pull requests + +## Authors + +* Oleg Bartunov +* Teodor Sigaev +* Alexand Korotkov + +## Regards + +Development is sponsored by [Wargaming.net] + +## License + +GPL diff --git a/doc/indexes.md b/doc/indexes.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/intro.md b/doc/intro.md new file mode 100644 index 0000000..3d6343e --- /dev/null +++ b/doc/intro.md @@ -0,0 +1,124 @@ +# Jsquery intro + +```jsquery``` implemented as datatype and `@@` operator. + +Search query has a form: + +```SQL +SELECT * FROM mytable + WHERE jsonb_column @@ 'jsquery_expression'; +``` + + +```jsquery_expression``` usually consists +of *path* and *value_expression*. + +For example if we are looking for: + +```json +{ + "user": { + "name": "Diego" + }, + "site": { + "url": "diego.com" + } +} +``` + +the expression is: + +``` +user.name = 'diego' +``` + +Sevral expressions could be connected using boolean `AND` & `OR` operators: + +``` +user.name = 'diego' AND site.url = 'diego.com' +``` + +Expression could be negated with `NOT` operator: + +``` +NOT user.name = 'diego' +``` + +JSON value could be one following types: + +* array +* numeric +* object +* string +* boolean + +You can check type using `is` operator: + +``` +user.name is array +user.name is numeric +user.name is object +user.name is string +user.name is boolean +``` + +For all types you `=` (equality) operator is defined: + +``` +user.roles = ["admin","root"] +user.age = 3 +user.active = true +user.address = {city: "SPb"} +user.name = "diego" +``` + +For numerics there are expected comparison operators: + +``` +x > 1 AND x < 10 +x >= 1 AND x <= 10 +``` + +To check that scalar value belongs to some list: + +```sql +select '{a: 2}'::jsonb @@ 'a IN (1,2,5)'; +``` + +For arrays there are convenient operators: + +```sql +-- overlap +select '{"a": {"b": [1,2,3]}}'::jsonb @@ 'a.b && [1,2,5]'; + +-- contains +select '{"a": {"b": [1,2,3]}}'::jsonb @@ 'a.b @> [1,2]'; + +-- contained +select '{"a": {"b": [1,2,3]}}'::jsonb @@ 'a.b <@ [1,2,3,4,5]' +``` + +If you just want to check that some path exists in json document use `=*`: + +select '{"a": {"b": [1,2,3]}}'::jsonb @@ 'a.b = *' + + +Path expression supports wild cards: + +`#` - any alement of array +`%` - any key in object +`*` - any path + +```sql +select '{a: {b: {c: 1}}}'::jsonb @@ '*.c = 1' +select '{a: {b: {c: 1}}}'::jsonb @@ 'a.%.c = 1' +select '{a: {b: [1,2]}}'::jsonb @@ 'a.b.# = 1' +``` + +jsquery expression could be expressed recursively using `()`: + +``` +address(city = "SPB" AND street = "Nevskiy") +``` + +This means eval `city = "SPB" AND street = "Nevskiy"` expression in context of `address` attribute. diff --git a/doc/jsquery.ebnf b/doc/jsquery.ebnf new file mode 100644 index 0000000..5424995 --- /dev/null +++ b/doc/jsquery.ebnf @@ -0,0 +1,75 @@ +result ::= ( expr | null) + +array ::= '[' value_list ']' + +scalar_value ::= ( + STRING_P + | IN_P + | IS_P + | OR_P + | AND_P + | NOT_P + | NULL_P + | TRUE_P + | ARRAY_T + | FALSE_P + | NUMERIC_T + | OBJECT_T + | STRING_T + | BOOLEAN_T + | NUMERIC_P ) + +value_list ::= (scalar_value | value_list ',' scalar_value) + +right_expr ::= ( + '='right_exprscalar_value + | IN_P '(' value_list ')' + | '=' array + | '=' '*' + | '<' NUMERIC_P + | '>' NUMERIC_P + | '<' '=' NUMERIC_P + | '>' '=' NUMERIC_P + | '@' '>' array + | '<' '@' array + | '&' '&' array + | IS_P ARRAY_T + | IS_P NUMERIC_T + | IS_P OBJECT_T + | IS_P STRING_T + | IS_P BOOLEAN_T ) + +expr ::= ( + path right_expr + | path HINT_P right_expr + | NOT_P expr + | NOT_P HINT_P right_expr + | NOT_P right_expr + | path '(' expr ')' + | '(' expr ')' + | expr AND_P expr + | expr OR_P expr) + +key ::= ( + '*' + | '#' + | '%' + | '$' + | STRING_P + | IN_P + | IS_P + | OR_P + | AND_P + | NULL_P + | TRUE_P + | ARRAY_T + | FALSE_P + | NUMERIC_T + | OBJECT_T + | STRING_T + | BOOLEAN_T + | NUMERIC_P ) + +key_any ::= ( key | NOT_P) + +path ::= ( key | path '.' key_any | NOT_P '.' key_any ) diff --git a/doc/operators.md b/doc/operators.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/optimizer.md b/doc/optimizer.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/syntax.md b/doc/syntax.md new file mode 100644 index 0000000..1b39533 --- /dev/null +++ b/doc/syntax.md @@ -0,0 +1,74 @@ +## Syntax + +```jsquery``` expresion usually consists of *path* and *value_expression*: + +```json +{ + "user": { + "name": "Diego" + }, + "site": { + "url": "diego.com" + } +} +``` + +``` +user.name = 'diego' +``` + + +```ebnf + expr ::= path value_expr + | path HINT value_expr + | NOT expr + | NOT HINT value_expr + | NOT value_expr + | path '(' expr ')' + | '(' expr ')' + | expr AND expr + | expr OR expr + + value_expr ::= '=' scalar_value + | IN '(' value_list ')' + | '=' array + | '=' '*' + | '<' NUMERIC + | '<' '=' NUMERIC + | '>' NUMERIC + | '>' '=' NUMERIC + | '@' '>' array + | '<' '@' array + | '&' '&' array + | IS ARRAY + | IS NUMERIC + | IS OBJECT + | IS STRING + | IS BOOLEAN + + path ::= key + | path '.' key_any + | NOT '.' key_any + + key ::= '*' + | '#' + | '%' + | '$' + | STRING + + key_any ::= key + | NOT + + value_list ::= scalar_value + | value_list ',' scalar_value + + array ::= '[' value_list ']' + + scalar_value ::= null + | STRING + | true + | false + | NUMERIC + | OBJECT +``` +