diff --git a/CHANGELOG.md b/CHANGELOG.md index 00d2d7309442f1d3e9be2959c1f493d56e893461..73433a48ceb379ffc792b86613ecdbf158793727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### - [#203](https://gitlab.com/caosdb/caosdb-server/-/issues/203) +* Searching for values in scientific notation + [#143](https://gitlab.com/caosdb/caosdb-server/-/issues/143) * Denying a role permission has no effect [#196](https://gitlab.com/caosdb/caosdb-server/-/issues/196). See security notes below. diff --git a/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java b/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java index 352a7428f2007fc59cbf0b3fb7f067d1198857c1..5c9f4c2ca13f8481d075117cb34dec41e5aeeab8 100644 --- a/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java +++ b/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java @@ -46,7 +46,7 @@ public class ExecuteQuery extends FlagJob { try { queryInstance.execute(getTransaction().getAccess()); } catch (final ParsingException e) { - getContainer().addError(ServerMessages.QUERY_PARSING_ERROR); + getContainer().addError(ServerMessages.QUERY_PARSING_ERROR(e.getMessage())); } catch (final UnsupportedOperationException e) { getContainer().addError(ServerMessages.QUERY_EXCEPTION); getContainer() diff --git a/src/main/java/org/caosdb/server/query/CQLLexer.g4 b/src/main/java/org/caosdb/server/query/CQLLexer.g4 index 518d1628b16ab9d0d66816b88e9b9115b765b6d4..be6dcfa99d2f9c48cba8a0f7fccb7f97b277e01d 100644 --- a/src/main/java/org/caosdb/server/query/CQLLexer.g4 +++ b/src/main/java/org/caosdb/server/query/CQLLexer.g4 @@ -522,31 +522,55 @@ TODAY: [Tt][Oo][Dd][Aa][Yy] WHITE_SPACE_f? ; +/** */ +COLON: + ':' +; + +/** Matches signed and unsigned numbers with decimal points, also numbers in scientific notation. */ +DECIMAL_NUMBER: + ((HYPHEN_f | PLUS ) WHITE_SPACE_f?)? + ( NUM_f? DOT NUM_f WHITE_SPACE_f? E_NOTATION_f? + | NUM_f WHITE_SPACE_f? E_NOTATION_f + ) +; + /** */ HYPHEN: - '-' + HYPHEN_f ; /** */ -COLON: - ':' +PLUS: + '+' ; /** */ -NUM: +fragment +HYPHEN_f: + '-' +; + +/** Matches only unsigned integer numbers. */ +UNSIGNED_INT: NUM_f - | DOT NUM_f - | NUM_f DOT NUM_f ; /** */ +fragment NUM_f: ('0'..'9')+ ; +/** */ +fragment +E_NOTATION_f: + [Ee] WHITE_SPACE_f? [+-]? WHITE_SPACE_f? NUM_f +; + /** */ TXT: - ('a'..'z' | 'A'..'Z' | '0'..'9' | '_' | '-' {_input.LA(1) != '>'}? | '+' | '&' | ';' | ',' | '$' | ':' | '%' | '^' | '~' {_input.LA(1) != '='}? | '`' | '´' | 'ö' | 'ä' | 'ß' | 'ü' | 'Ö' | 'Ä' | 'Ü' | '@' | '[' | ']' | '{' | '}' )+ + ('a'..'z' | 'A'..'Z' | NUM_f | '_' | '-' {_input.LA(1) != '>'}? | PLUS | '&' | ';' | ',' | '$' | ':' | '%' | '^' | '~' {_input.LA(1) != '='}? | '`' | '´' | 'ö' | 'ä' | 'ß' | 'ü' | 'Ö' | 'Ä' | 'Ü' | '@' | '[' | ']' | '{' | '}' )+ ; /** */ diff --git a/src/main/java/org/caosdb/server/query/CQLParser.g4 b/src/main/java/org/caosdb/server/query/CQLParser.g4 index d5e375102d79d395b1f06c62c5aef9537092b0fc..8615bae31fae67eb0bd3315f664bfa708db61618 100644 --- a/src/main/java/org/caosdb/server/query/CQLParser.g4 +++ b/src/main/java/org/caosdb/server/query/CQLParser.g4 @@ -282,20 +282,20 @@ transaction_time returns [String tqp, String op] */ datetime : - NUM // year + UNSIGNED_INT // year ( - HYPHEN NUM // mon + HYPHEN UNSIGNED_INT // mon ( - HYPHEN NUM // day of mon + HYPHEN UNSIGNED_INT // day of mon ( (m=TXT {$m.text.equals("T")}?)?// compliance with iso datetime - NUM // hour + UNSIGNED_INT // hour ( - COLON NUM // minut + COLON UNSIGNED_INT // minut ( - COLON NUM // sec + COLON UNSIGNED_INT // sec ( - DOT NUM // millisec + DOT UNSIGNED_INT // millisec )? )? )? @@ -581,7 +581,7 @@ property returns [Query.Pattern pp, String agg]locals [StringBuffer sb] | like_pattern {$pp = $like_pattern.ep;} | ( double_quoted {$pp = $double_quoted.ep;} ) | ( single_quoted {$pp = $single_quoted.ep;} ) - | ((m=TXT | m=NUM | m=REGEXP_MARKER | m=ENTITY){$sb.append($m.text);})+ {$pp = new Query.Pattern($sb.toString(), Query.Pattern.TYPE_NORMAL);} + | ((m=TXT | m=UNSIGNED_INT | m=UNSIGNED_DECIMAL_NUMBER | m=REGEXP_MARKER | m=ENTITY){$sb.append($m.text);})+ {$pp = new Query.Pattern($sb.toString(), Query.Pattern.TYPE_NORMAL);} ) WHITE_SPACE? ; @@ -613,9 +613,8 @@ value returns [String str] */ number_with_unit : - HYPHEN?? - NUM - (WHITE_SPACE?? unit)? + ( UNSIGNED_INT | DECIMAL_NUMBER | ( HYPHEN | PLUS ) WHITE_SPACE? UNSIGNED_INT) + (WHITE_SPACE? unit)? ; /** @@ -626,7 +625,7 @@ unit (~(WHITE_SPACE | WHICH | HAS_A | WITH_A | WHERE | DOT | AND | OR | RPAREN )) (~(WHITE_SPACE))* | - NUM SLASH (~(WHITE_SPACE))+ + UNSIGNED_INT SLASH (~(WHITE_SPACE))+ ; /** diff --git a/src/main/java/org/caosdb/server/query/POV.java b/src/main/java/org/caosdb/server/query/POV.java index 89ea9ad46e6205dfe0583e8f54551effd82a2658..0de160cdb6249ffaea10f4eeeafdf4884323a186 100644 --- a/src/main/java/org/caosdb/server/query/POV.java +++ b/src/main/java/org/caosdb/server/query/POV.java @@ -51,6 +51,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class POV implements EntityFilterInterface { + public static final Pattern NUMBER_PATTERN = + Pattern.compile( + "^((?:[+-]\\s*)?[0-9]+(?:\\.[0-9]+)?(?:\\s*[eE]\\s*[+-]?\\s*[0-9]+)?)\\s*([^-]*)$"); private SubProperty subp = null; public static int retry = 10; private int retry_count = 0; @@ -116,14 +119,22 @@ public class POV implements EntityFilterInterface { // try and parse as integer try { - final Pattern dp = Pattern.compile("^(-?[0-9]++)\\s*([^(\\.[0-9])-][^-]*)?$"); - final Matcher m = dp.matcher(value); + final Matcher m = NUMBER_PATTERN.matcher(this.value); if (!m.matches()) { throw new NumberFormatException(); } final String vIntStr = m.group(1); + this.vInt = Integer.parseInt(vIntStr.replaceAll("\\s", "")); + if (vIntStr.matches(".*\\s.*")) { + // empty space in scientific notation is a common typo + throw new Query.ParsingException( + "You typed \"" + + vIntStr + + "\". Empty spaces are not allowed in numbers. Did you mean \"" + + vIntStr.replaceAll("\\s", "") + + "\"?"); + } this.unitStr = m.group(2); - this.vInt = Integer.parseInt(vIntStr); } catch (final NumberFormatException e) { this.vInt = null; } @@ -132,15 +143,23 @@ public class POV implements EntityFilterInterface { if (this.vInt == null) { try { // Doubles are allowed without dots, for example when the integer overflows. - final Pattern dp = Pattern.compile("^(-?[0-9]+(?:\\.)?(?:[0-9]+))\\s*([^-]*)$"); - final Matcher m = dp.matcher(value); + final Matcher m = NUMBER_PATTERN.matcher(this.value); if (!m.matches()) { throw new NumberFormatException(); } final String vDoubleStr = m.group(1); - this.unitStr = m.group(2); - this.vDouble = Double.parseDouble(vDoubleStr); + this.vDouble = Double.parseDouble(vDoubleStr.replaceAll("\\s", "")); + if (vDoubleStr.matches(".*\\s.*")) { + // empty space in scientific notation is a common typo + throw new Query.ParsingException( + "You typed \"" + + vDoubleStr + + "\". Empty spaces are not allowed in numbers. Did you mean \"" + + vDoubleStr.replaceAll("\\s", "") + + "\"?"); + } + this.unitStr = m.group(2); } catch (final NumberFormatException e) { this.vDouble = null; } @@ -153,7 +172,7 @@ public class POV implements EntityFilterInterface { this.unit = getUnit(this.unitStr); } catch (final ParserException e) { e.printStackTrace(); - throw new UnsupportedOperationException("Could not parse the unit."); + throw new UnsupportedOperationException("Could not parse the unit:"); } this.stdUnitSig = this.unit.normalize().getSignature(); diff --git a/src/main/java/org/caosdb/server/utils/ServerMessages.java b/src/main/java/org/caosdb/server/utils/ServerMessages.java index 835087ce8825de6c136dabd746534bc42f3240d5..9e18ca9065ec407d2dfc795d173e8d97f11682be 100644 --- a/src/main/java/org/caosdb/server/utils/ServerMessages.java +++ b/src/main/java/org/caosdb/server/utils/ServerMessages.java @@ -617,4 +617,8 @@ public class ServerMessages { return new Message( MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "You cannot delete yourself."); } + + public static Message QUERY_PARSING_ERROR(String message) { + return new Message(MessageType.Error, MessageCode.MESSAGE_CODE_QUERY_PARSING_ERROR, message); + } } diff --git a/src/test/java/org/caosdb/server/query/POVTest.java b/src/test/java/org/caosdb/server/query/POVTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7212423bbaf2ac4d3c5e33543d3e6ffe4e70b5e4 --- /dev/null +++ b/src/test/java/org/caosdb/server/query/POVTest.java @@ -0,0 +1,77 @@ +package org.caosdb.server.query; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.regex.Matcher; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class POVTest { + + public static final String MAX_INT = "2147483647"; + public static final String MIN_INT = "-2147483648"; + public static final String MAX_DOUBLE = "1.7976931348623157E308"; + public static final String MIN_DOUBLE = "4.9E-324"; + + @ParameterizedTest + @ValueSource(strings = {"16", MAX_INT, MIN_INT, "0", "-0", "1", "- 1", "-1", "+1"}) + void testNumberPatternMatchInteger(String intValue) { + + Matcher matcher = POV.NUMBER_PATTERN.matcher(intValue); + assertTrue(matcher.matches()); + assertEquals(intValue, matcher.group(1).toString()); + + Integer.valueOf(intValue.replaceAll("\\s", "")); + + Matcher matcherWithUnit = POV.NUMBER_PATTERN.matcher(intValue + " m^2"); + assertTrue(matcherWithUnit.matches()); + assertEquals(intValue, matcherWithUnit.group(1).toString()); + assertEquals("m^2", matcherWithUnit.group(2).toString()); + + Matcher matcherWithStrangeUnit = POV.NUMBER_PATTERN.matcher(intValue + " e"); + assertTrue(matcherWithStrangeUnit.matches()); + assertEquals(intValue, matcherWithStrangeUnit.group(1).toString()); + assertEquals("e", matcherWithStrangeUnit.group(2).toString()); + } + + @ParameterizedTest + @ValueSource( + strings = { + "1.2123e+3", + "1.21234E+3", + "5.213e2", + "5.2234E2", + "16.0", + MAX_DOUBLE, + MIN_DOUBLE, + "0.0", + "-0.0", + "1.2", + "- 1.2", + "-1.2", + "2e-323", + "2E-323", + "2E- 323", + "2 e -323", + "+ 2.2132e+23" + }) + void testNumberPatternMatchDouble(String doubleValue) { + + Matcher matcher = POV.NUMBER_PATTERN.matcher(doubleValue); + assertTrue(matcher.matches()); + assertEquals(doubleValue, matcher.group(1).toString()); + + Double.valueOf(doubleValue.replaceAll("\\s", "")); + + Matcher matcherWithUnit = POV.NUMBER_PATTERN.matcher(doubleValue + " m^2"); + assertTrue(matcherWithUnit.matches()); + assertEquals(doubleValue, matcherWithUnit.group(1).toString()); + assertEquals("m^2", matcherWithUnit.group(2).toString()); + + Matcher matcherWithStrangeUnit = POV.NUMBER_PATTERN.matcher(doubleValue + " e"); + assertTrue(matcherWithStrangeUnit.matches()); + assertEquals(doubleValue, matcherWithStrangeUnit.group(1).toString()); + assertEquals("e", matcherWithStrangeUnit.group(2).toString()); + } +} diff --git a/src/test/java/org/caosdb/server/query/TestCQL.java b/src/test/java/org/caosdb/server/query/TestCQL.java index 6d8f015d158aafe86baa087d6b56d38c0283ec62..8d7a7e8fb219f286d6cdaffc4b28240a7b2897d5 100644 --- a/src/test/java/org/caosdb/server/query/TestCQL.java +++ b/src/test/java/org/caosdb/server/query/TestCQL.java @@ -25,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -46,6 +47,8 @@ import org.caosdb.server.query.Query.QueryException; import org.caosdb.server.utils.Initialization; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class TestCQL { @@ -272,6 +275,7 @@ public class TestCQL { String issue131d = "FIND ename WITH (pname1.x) AND pname2"; String issue131e = "FIND ename WITH (pname1.pname2 > 30) AND (pname1.pname2 < 40)"; String issue131f = "FIND ename WITH (pname1.pname2 > 30) AND pname1.pname2 < 40"; + String issue144 = "FIND ename WITH pname = "; // https://gitlab.com/caosdb/caosdb-server/-/issues/130 String issue130a = @@ -7016,6 +7020,57 @@ public class TestCQL { assertEquals("POV(null,LIKE ,%\"'bla%)", conj.getFilters().get(5).toString()); } + @ParameterizedTest + @ValueSource( + strings = { + "1e+23", + "1E+23", + "5e22", + "5E22", + "2e-323", + "2E-323", + "-123", + "-1e23", + "3E15m^2", + "-3e15m", + "-3e15 1/s", + "3e15 m^2", + "+1", + "+2.234", + "+2.234e+23", + "+3.324E-23" + }) + public void testIssue144(String scientific_notation) { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.issue144 + scientific_notation)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + for (final Token t : tokens.getTokens()) { + System.out.println(t.toString()); + } + + System.out.println(sfq.toStringTree(parser)); + + assertTrue(sfq.filter instanceof POV); + POV pov = (POV) sfq.filter; + assertEquals("POV(pname,=," + scientific_notation + ")", pov.toString()); + assertTrue(pov.getVDouble() != null || pov.getVInt() != null); + } + + @ParameterizedTest + @ValueSource(strings = {"- 123", "- 1e23", "2 e -23", "2E- 323", "+ 1"}) + public void testIssue144WhiteSpaceInNumber(String number) { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.issue144 + number)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + assertThrowsExactly(Query.ParsingException.class, () -> parser.cq()); + } + /** * Test that brackets around 'has pname' do not cause filter to become subproperty filter. *