diff --git a/CHANGELOG.md b/CHANGELOG.md index c25c710e731a0d045ab75a12cb70ee90531d3b1f..df5b85f849385868b6eb8e1d7daa605bc1b4e8d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Basic caching for queries. The caching is enabled by default and can be controlled by the usual "cache" flag. * Documentation for the overall server structure. +* Add `BEFORE`, `AFTER`, `UNTIL`, `SINCE` keywords for query transaction ### Changed @@ -30,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* #131 - CQL Parsing error when white space characters before some units. +* #134 - CQL Parsing error when multiple white space characters after `FROM`. * #130 - Error during `FIND ENTITY` when `QUERY_FILTER_ENTITIES_WITHOUT_RETRIEVE_PERMISSIONS=False`. * #125 - `bend_symlinks` script did not allow whitespace in filename. diff --git a/README.md b/README.md index 21d5400e5383d4c2571f8a409f50cd72f926187a..62da38e86df18a34deddf7c31a651dd12d81b7e8 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ when creating the merge request. This allows our team to work with you on your r - If you have a suggestion for the [documentation](https://docs.indiscale.com/caosdb-server/), the preferred way is also a merge request as describe above (the documentation resides in `src/doc`). However, you can also create an issue for it. -- You can also contact us at **info (AT) caosdb.de**. +- You can also contact us at **info (AT) caosdb.de** and join the + CaosDB community on + [#caosdb:matrix.org](https://matrix.to/#/!unwwlTfOznjEnMMXxf:matrix.org). ## License diff --git a/doc/Query.md b/doc/Query.md index 4ff07fbad5c8c6acffac9d6c57e325ed92c8a9cd..1d7748438cd7f1a4a84f0ab66215564cbc5ad36d 100644 --- a/doc/Query.md +++ b/doc/Query.md @@ -226,14 +226,13 @@ The following query returns entities which have a _pname1_ property with any val ### TransactionFilter *Definition* - sugar:: `HAS BEEN` | `HAVE BEEN` | `HAD BEEN` | `WAS` | `IS` | + sugar:: `HAS BEEN` | `HAVE BEEN` | `HAD BEEN` | `WAS` | `IS` negated_sugar:: `HAS NOT BEEN` | `HASN'T BEEN` | `WAS NOT` | `WASN'T` | `IS NOT` | `ISN'T` | `HAVN'T BEEN` | `HAVE NOT BEEN` | `HADN'T BEEN` | `HAD NOT BEEN` by_clause:: `BY (ME | username | SOMEONE ELSE (BUT ME)? | SOMEONE ELSE BUT username)` - date:: A date string of the form `YYYY-MM-DD` - datetime:: A datetime string of the form `YYYY-MM-DD hh:mm:ss` - time_clause:: `ON ($date|$datetime) ` Here is plenty of room for more syntactic sugar, e.g. a `TODAY` keyword, and more funcionality, e.g. ranges. + datetime:: A datetime string of the form `YYYY[-MM[-DD(T| )[hh[:mm[:ss[.nnn][(+|-)zzzz]]]]]]` + time_clause:: `[AT|ON|IN|BEFORE|AFTER|UNTIL|SINCE] (datetime) ` -`FIND ename WHICH ($sugar|$negated_sugar)? (NOT)? (CREATED|INSERTED|UPDATED|DELETED) (by_clause time_clause?| time_clause by_clause?)` +`FIND ename WHICH (sugar|negated_sugar)? (NOT)? (CREATED|INSERTED|UPDATED) (by_clause time_clause?| time_clause by_clause?)` *Examples* @@ -247,8 +246,9 @@ The following query returns entities which have a _pname1_ property with any val `FIND ename WHICH HAS BEEN CREATED BY erwin` -`FIND ename . CREATED BY erwin ON ` +`FIND ename WHICH HAS BEEN INSERTED SINCE 2021-04` +Note that `SINCE` and `UNTIL` are inclusive, while `BEFORE` and `AFTER` are not. ### File Location diff --git a/src/main/java/org/caosdb/server/query/CQLLexer.g4 b/src/main/java/org/caosdb/server/query/CQLLexer.g4 index 85061d30cacb222dd8c866d84a7abfb525a592a9..71b41d480bc171aa02af3b2e61eadd16345b3c6d 100644 --- a/src/main/java/org/caosdb/server/query/CQLLexer.g4 +++ b/src/main/java/org/caosdb/server/query/CQLLexer.g4 @@ -77,6 +77,22 @@ IN: [Ii][Nn] WHITE_SPACE_f? ; +AFTER: + [Aa][Ff][Tt][Ee][Rr] WHITE_SPACE_f? +; + +BEFORE: + [Bb][Ee][Ff][Oo][Rr][Ee] WHITE_SPACE_f? +; + +UNTIL: + [Uu][Nn][Tt][Ii][Ll] WHITE_SPACE_f? +; + +SINCE: + [Ss][Ii][Nn][Cc][Ee] WHITE_SPACE_f? +; + IS_STORED_AT: (IS_f WHITE_SPACE_f?)? [Ss][Tt][Oo][Rr][Ee][Dd] (WHITE_SPACE_f? AT)? WHITE_SPACE_f? ; @@ -486,7 +502,7 @@ mode DOUBLE_QUOTE_MODE; mode SELECT_MODE; FROM: - [Ff][Rr][Oo][Mm]([ \t\n\r])? -> mode(DEFAULT_MODE) + [Ff][Rr][Oo][Mm]([ \t\n\r])* -> mode(DEFAULT_MODE) ; SELECT_DOT: diff --git a/src/main/java/org/caosdb/server/query/CQLParser.g4 b/src/main/java/org/caosdb/server/query/CQLParser.g4 index 452321c670d4101bc603e657023143025f12c22f..6aea8c55f006cd9127d68f0c09e1654d9f9842a9 100644 --- a/src/main/java/org/caosdb/server/query/CQLParser.g4 +++ b/src/main/java/org/caosdb/server/query/CQLParser.g4 @@ -153,14 +153,15 @@ idfilter returns [IDFilter filter] locals [String o, String v, String a] )? ; -transaction returns [TransactionFilter filter] locals [String type, TransactionFilter.Transactor user, String time] +transaction returns [TransactionFilter filter] locals [String type, TransactionFilter.Transactor user, String time, String time_op] @init{ $time = null; $user = null; $type = null; + $time_op = null; } @after{ - $filter = new TransactionFilter($type,$user,$time); + $filter = new TransactionFilter($type,$user,$time,$time_op); } : ( @@ -169,8 +170,8 @@ transaction returns [TransactionFilter filter] locals [String type, TransactionF ) ( - transactor (transaction_time {$time = $transaction_time.tqp;})? {$user = $transactor.t;} - | transaction_time (transactor {$user = $transactor.t;})? {$time = $transaction_time.tqp;} + transactor (transaction_time {$time = $transaction_time.tqp; $time_op = $transaction_time.op;})? {$user = $transactor.t;} + | transaction_time (transactor {$user = $transactor.t;})? {$time = $transaction_time.tqp; $time_op = $transaction_time.op;} ) ; @@ -199,12 +200,25 @@ username returns [Query.Pattern ep] locals [int type] ( STAR {$type = Query.Pattern.TYPE_LIKE;} | ~(STAR | WHITE_SPACE) )+ ; -transaction_time returns [String tqp] +transaction_time returns [String tqp, String op] +@init { + $op = "("; +} : + ( + AT {$op = "=";} + | (ON | IN) + | ( + BEFORE {$op = "<";} + | UNTIL {$op = "<=";} + | AFTER {$op = ">";} + | SINCE {$op = ">=";} + ) + )? ( - (ON | IN) - (value {$tqp = $value.text;}) - ) | TODAY {$tqp = TransactionFilter.TODAY;} + TODAY {$tqp = TransactionFilter.TODAY;} + | value {$tqp = $value.text;} + ) ; /* @@ -481,8 +495,10 @@ number_with_unit unit : - TXT - | NUM SLASH TXT + (~(WHITE_SPACE | WHICH | HAS_A | WITH | WHERE | DOT | AND | OR )) + (~(WHITE_SPACE))* + | + NUM SLASH (~(WHITE_SPACE))+ ; location returns [String str] diff --git a/src/main/java/org/caosdb/server/query/POV.java b/src/main/java/org/caosdb/server/query/POV.java index a1008bd6a67a8712baf2c9b52ab164f619236263..b1a457529a0199edcc5061110ee97e416a264fff 100644 --- a/src/main/java/org/caosdb/server/query/POV.java +++ b/src/main/java/org/caosdb/server/query/POV.java @@ -70,8 +70,8 @@ public class POV implements EntityFilterInterface { private String propertiesTable = null; private String refIdsTable = null; private final HashMap<String, String> statistics = new HashMap<>(); - private Logger logger = LoggerFactory.getLogger(getClass()); - private Stack<String> prefix = new Stack<>(); + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Stack<String> prefix = new Stack<>(); private Unit getUnit(final String s) throws ParserException { return CaosDBSystemOfUnits.getUnit(s); @@ -116,7 +116,7 @@ public class POV implements EntityFilterInterface { // try and parse as integer try { - final Pattern dp = Pattern.compile("^(-?[0-9]++)([^(\\.[0-9])-][^-]*)?$"); + final Pattern dp = Pattern.compile("^(-?[0-9]++)\\s*([^(\\.[0-9])-][^-]*)?$"); final Matcher m = dp.matcher(value); if (!m.matches()) { throw new NumberFormatException(); @@ -133,7 +133,7 @@ public class POV implements EntityFilterInterface { this.vDouble = (double) this.vInt; } else { try { - final Pattern dp = Pattern.compile("^(-?[0-9]+(?:\\.[0-9]+))([^-]*)$"); + final Pattern dp = Pattern.compile("^(-?[0-9]+(?:\\.[0-9]+))\\s*([^-]*)$"); final Matcher m = dp.matcher(value); if (!m.matches()) { throw new NumberFormatException(); @@ -142,7 +142,7 @@ public class POV implements EntityFilterInterface { unitStr = m.group(2); this.vDouble = Double.parseDouble(vDoubleStr); - if ((this.vDouble % 1) == 0) { + if (this.vDouble % 1 == 0) { this.vInt = (int) Math.floor(this.vDouble); } } catch (final NumberFormatException e) { @@ -509,14 +509,16 @@ public class POV implements EntityFilterInterface { return this.aggregate; } - private String measurement(String m) { + private String measurement(final String m) { return String.join("", prefix) + m; } @Override public String getCacheKey() { - StringBuilder sb = new StringBuilder(); - if (this.getAggregate() != null) sb.append(this.aggregate); + final StringBuilder sb = new StringBuilder(); + if (this.getAggregate() != null) { + sb.append(this.aggregate); + } sb.append(toString()); if (this.hasSubProperty()) { sb.append(getSubProperty().getCacheKey()); diff --git a/src/main/java/org/caosdb/server/query/TransactionFilter.java b/src/main/java/org/caosdb/server/query/TransactionFilter.java index fed4a7156048f1bd43f7e4000df80f29a38187a0..2099639b0fc862edb2fc0bceecfa17bad6dad167 100644 --- a/src/main/java/org/caosdb/server/query/TransactionFilter.java +++ b/src/main/java/org/caosdb/server/query/TransactionFilter.java @@ -29,7 +29,6 @@ import java.sql.Types; import org.caosdb.datetime.Date; import org.caosdb.datetime.DateTimeFactory2; import org.caosdb.datetime.Interval; -import org.caosdb.datetime.SemiCompleteDateTime; import org.caosdb.datetime.UTCDateTime; import org.caosdb.server.accessControl.Principal; import org.caosdb.server.accessControl.UserSources; @@ -96,14 +95,20 @@ public class TransactionFilter implements EntityFilterInterface { } } - public TransactionFilter(final String type, final Transactor transactor, final String time) { + public TransactionFilter( + final String type, + final Transactor transactor, + final String time, + final String timeOperator) { this.transactor = transactor; this.transactionTime = time; this.transactionType = type; + this.transactionTimeOperator = timeOperator; } private final Transactor transactor; private final String transactionTime; + private final String transactionTimeOperator; private final String transactionType; @Override @@ -123,7 +128,7 @@ public class TransactionFilter implements EntityFilterInterface { } else { try { - dt = (SemiCompleteDateTime) DateTimeFactory2.valueOf(this.transactionTime); + dt = (Interval) DateTimeFactory2.valueOf(this.transactionTime); } catch (final ClassCastException e) { throw new QueryException("Transaction time must be a SemiCompleteDateTime."); } catch (final IllegalArgumentException e) { @@ -201,14 +206,13 @@ public class TransactionFilter implements EntityFilterInterface { } else { prepareCall.setNull(10, Types.INTEGER); } - prepareCall.setString(11, "("); // '(' means 'is in the // interval' } else { prepareCall.setNull(9, Types.BIGINT); prepareCall.setNull(10, Types.INTEGER); - prepareCall.setNull(11, Types.CHAR); } + prepareCall.setString(11, transactionTimeOperator); } else { // ilb_sec, ilb_nanos, eub_sec, eub_nanos, operator_t prepareCall.setNull(7, Types.BIGINT); @@ -251,6 +255,8 @@ public class TransactionFilter implements EntityFilterInterface { return "TRANS(" + this.transactionType + "," + + this.transactionTimeOperator + + "," + this.transactionTime + "," + this.transactor diff --git a/src/test/java/org/caosdb/server/query/TestCQL.java b/src/test/java/org/caosdb/server/query/TestCQL.java index 2c28d7f59c018f90ce77e9f7691a362027655466..154977a909a9d34ac2034c1ef352b81e7b2a3cb7 100644 --- a/src/test/java/org/caosdb/server/query/TestCQL.java +++ b/src/test/java/org/caosdb/server/query/TestCQL.java @@ -237,6 +237,10 @@ public class TestCQL { String queryIssue31 = "FIND FILE WHICH IS STORED AT /data/in0.foo"; String queryIssue116 = "FIND *"; + String queryIssue132a = "FIND ENTITY WHICH HAS BEEN INSERTED AFTER TODAY"; + String queryIssue132b = "FIND ENTITY WHICH HAS BEEN CREATED TODAY BY ME"; + String queryIssue134 = "SELECT pname FROM ename"; + String queryIssue131 = "FIND ENTITY WITH pname = 13 €"; // File paths /////////////////////////////////////////////////////////////// String filepath_verb01 = "/foo/"; @@ -5692,7 +5696,7 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); assertTrue(sfq.filter instanceof TransactionFilter); - assertEquals("TRANS(Insert,null,Transactor(some%,=))", sfq.filter.toString()); + assertEquals("TRANS(Insert,null,null,Transactor(some%,=))", sfq.filter.toString()); } /** String ticket242 = "FIND RECORD WHICH HAS been created by some.user"; */ @@ -5707,7 +5711,7 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - assertEquals("TRANS(Insert,null,Transactor(some.user,=))", sfq.filter.toString()); + assertEquals("TRANS(Insert,null,null,Transactor(some.user,=))", sfq.filter.toString()); assertTrue(sfq.filter instanceof TransactionFilter); } @@ -5781,7 +5785,7 @@ public class TestCQL { assertEquals("@(null,null)", n.getFilter().toString()); assertTrue(f2.getLast() instanceof TransactionFilter); - assertEquals("TRANS(Insert,null,Transactor(null,=))", f2.getLast().toString()); + assertEquals("TRANS(Insert,null,null,Transactor(null,=))", f2.getLast().toString()); } /** String ticket262e = "COUNT FILE WHICH IS NOT REFERENCED AND WAS created by me"; */ @@ -5804,7 +5808,7 @@ public class TestCQL { assertEquals("@(null,null)", n.getFilter().toString()); assertTrue(f2.getLast() instanceof TransactionFilter); - assertEquals("TRANS(Insert,null,Transactor(null,=))", f2.getLast().toString()); + assertEquals("TRANS(Insert,null,null,Transactor(null,=))", f2.getLast().toString()); } /** String ticket262f = "COUNT FILE WHICH IS NOT REFERENCED BY entity AND WAS created by me"; */ @@ -5827,7 +5831,7 @@ public class TestCQL { assertEquals("@(entity,null)", n.getFilter().toString()); assertTrue(f2.getLast() instanceof TransactionFilter); - assertEquals("TRANS(Insert,null,Transactor(null,=))", f2.getLast().toString()); + assertEquals("TRANS(Insert,null,null,Transactor(null,=))", f2.getLast().toString()); } /** @@ -5853,7 +5857,7 @@ public class TestCQL { assertEquals("@(entity,null)", n.getFilter().toString()); assertTrue(f2.getLast() instanceof TransactionFilter); - assertEquals("TRANS(Insert,null,Transactor(null,=))", f2.getLast().toString()); + assertEquals("TRANS(Insert,null,null,Transactor(null,=))", f2.getLast().toString()); } /** String ticket262h = "COUNT FILE WHICH IS NOT REFERENCED BY entity WHICH WAS created by me"; */ @@ -5876,7 +5880,7 @@ public class TestCQL { assertNotNull(((Backreference) backref).getSubProperty()); assertEquals( - "TRANS(Insert,null,Transactor(null,=))", + "TRANS(Insert,null,null,Transactor(null,=))", ((Backreference) backref).getSubProperty().getFilter().toString()); } @@ -5917,7 +5921,7 @@ public class TestCQL { assertNotNull(((Backreference) backref).getSubProperty()); assertEquals( - "TRANS(Insert,null,Transactor(null,=))", + "TRANS(Insert,null,null,Transactor(null,=))", ((Backreference) backref).getSubProperty().getFilter().toString()); } @@ -6686,4 +6690,55 @@ public class TestCQL { assertEquals("POV(pname,=,with)", sfq.filter.toString()); assertNull(((POV) sfq.filter).getSubProperty()); } + + @Test + /** String queryIssue132a = "FIND ENTITY WHICH HAS BEEN INSERTED AFTER TODAY"; */ + public void testIssue132a() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.queryIssue132a)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertEquals("TRANS(Insert,>,Today,null)", sfq.filter.toString()); + } + + @Test + /** String queryIssue132b = "FIND ENTITY WHICH HAS BEEN CREATED TODAY BY ME"; */ + public void testIssue132b() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.queryIssue132b)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertEquals("TRANS(Insert,(,Today,Transactor(null,=))", sfq.filter.toString()); + } + + /** + * Multiple white space chars after `FROM`. + * + * <p>String queryIssue134 = "SELECT pname FROM ename"; + */ + @Test + public void testIssue134() { + // must not throw ParsingException + new Query(this.queryIssue134).parse(); + } + /** + * Space before special character unit + * + * <p>String queryIssue131= "FIND ENTITY WITH pname = 13 €"; + */ + @Test + public void testIssue131() { + // must not throw ParsingException + new Query(this.queryIssue131).parse(); + } }