diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index 59584213e..53026b864 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -178,4 +178,5 @@ public interface ExpressionVisitor { public void visit(NotExpression aThis); + void visit(SubstringExpression substringExpression); } diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index f78b1c048..527b4b052 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -493,4 +493,13 @@ public void visit(DateTimeLiteralExpression literal) { } + @Override + public void visit(SubstringExpression substringExpression) { + substringExpression.getExpression().accept(this); + substringExpression.getFromExpression().accept(this); + Expression forExpression = substringExpression.getForExpression(); + if (forExpression != null) { + forExpression.accept(this); + } + } } diff --git a/src/main/java/net/sf/jsqlparser/expression/SubstringExpression.java b/src/main/java/net/sf/jsqlparser/expression/SubstringExpression.java new file mode 100644 index 000000000..9a88385dc --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/SubstringExpression.java @@ -0,0 +1,74 @@ +/* + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2018 JSQLParser + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package net.sf.jsqlparser.expression; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +public class SubstringExpression extends ASTNodeAccessImpl implements Expression { + + private Expression expression; + private Expression fromExpression; + private Expression forExpression; + + @Override + public void accept(ExpressionVisitor expressionVisitor) { + expressionVisitor.visit(this); + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public Expression getFromExpression() { + return fromExpression; + } + + public void setFromExpression(Expression fromExpression) { + this.fromExpression = fromExpression; + } + + public Expression getForExpression() { + return forExpression; + } + + public void setForExpression(Expression forExpression) { + this.forExpression = forExpression; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("SUBSTRING("); + sb.append(expression); + sb.append(" FROM "); + sb.append(fromExpression); + if (forExpression != null) { + sb.append(" FOR "); + sb.append(forExpression); + } + sb.append(")"); + return sb.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 85df11aaa..1480a0a05 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -54,6 +54,7 @@ import net.sf.jsqlparser.expression.RowConstructor; import net.sf.jsqlparser.expression.SignedExpression; import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.SubstringExpression; import net.sf.jsqlparser.expression.TimeKeyExpression; import net.sf.jsqlparser.expression.TimeValue; import net.sf.jsqlparser.expression.TimestampValue; @@ -785,4 +786,14 @@ public void visit(Upsert upsert) { @Override public void visit(UseStatement use) { } + + @Override + public void visit(SubstringExpression substringExpression) { + substringExpression.getExpression().accept(this); + substringExpression.getFromExpression().accept(this); + Expression forExpression = substringExpression.getForExpression(); + if (forExpression != null) { + forExpression.accept(this); + } + } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index c8b53e28f..ed3314a38 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -54,6 +54,7 @@ import net.sf.jsqlparser.expression.RowConstructor; import net.sf.jsqlparser.expression.SignedExpression; import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.SubstringExpression; import net.sf.jsqlparser.expression.TimeKeyExpression; import net.sf.jsqlparser.expression.TimeValue; import net.sf.jsqlparser.expression.TimestampValue; @@ -760,4 +761,8 @@ public void visit(DateTimeLiteralExpression literal) { buffer.append(literal.toString()); } + @Override + public void visit(SubstringExpression substringExpression) { + buffer.append(substringExpression.toString()); + } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 79c29ec15..bb9cdc96c 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -260,6 +260,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */ | | | +| | | | @@ -960,7 +961,7 @@ String RelObjectNameWithoutValue() : | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= + | tk= | tk= | tk= | tk= ) { return tk.image; } @@ -2453,6 +2454,8 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(JsonExpression()) retval=JsonExpression() + | LOOKAHEAD(SubstringExpression()) retval=SubstringExpression() + | LOOKAHEAD(Function()) retval=Function() | token= { retval = new DoubleValue(token.image); } @@ -2809,6 +2812,22 @@ Execute Execute(): { } } +SubstringExpression SubstringExpression() : +{ + SubstringExpression retval = new SubstringExpression(); + Expression expr; + Expression fromExpression; + Expression forExpression; +} +{ + "(" + expr=SimpleExpression() { retval.setExpression(expr); } + fromExpression=AdditiveExpression() { retval.setFromExpression(fromExpression); } + [ forExpression=AdditiveExpression() { retval.setForExpression(forExpression); }] + ")" + { return retval; } +} + Function Function() #Function: { Function retval = new Function(); diff --git a/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java b/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java index 92117eacc..39a0217e4 100644 --- a/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java @@ -30,11 +30,8 @@ import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; import org.junit.After; import org.junit.AfterClass; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -217,4 +214,23 @@ public void testAnalyticFunctionWithoutExpression502() throws JSQLParserExceptio expr.accept(adapter); } + @Test + public void testSubstringFromFor() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseExpression("substring('asdf' from 2 for 3)"); + assertThat(expr, instanceOf(SubstringExpression.class)); + ExpressionVisitorAdapter adapter = new ExpressionVisitorAdapter(); + expr.accept(adapter); + } + + @Test + public void testSubstringFrom() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseExpression("substring('asdf' from 2)"); + assertThat(expr, instanceOf(SubstringExpression.class)); + ExpressionVisitorAdapter adapter = new ExpressionVisitorAdapter(); + try { + expr.accept(adapter); + } catch (NullPointerException e) { + fail("Tried to visit an optional for expression?"); + } + } } diff --git a/src/test/java/net/sf/jsqlparser/test/select/SelectTest.java b/src/test/java/net/sf/jsqlparser/test/select/SelectTest.java index 8168c61be..36276e8a8 100644 --- a/src/test/java/net/sf/jsqlparser/test/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/test/select/SelectTest.java @@ -2724,19 +2724,27 @@ public void testIssue554() throws JSQLParserException { public void testIssue567KeywordPrimary() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SELECT primary, secondary FROM info"); } - + public void testIssue572TaskReplacement() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SELECT task_id AS \"Task Id\" FROM testtable"); } - + public void testIssue566LargeView() throws IOException, JSQLParserException { String stmt = IOUtils.toString(SelectTest.class.getResourceAsStream("large-sql-issue-566.txt")); assertSqlCanBeParsedAndDeparsed(stmt, true); } - + public void testIssue566PostgreSQLEscaped() throws IOException, JSQLParserException { assertSqlCanBeParsedAndDeparsed("SELECT E'test'"); } + + public void testSubstringFrom() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT SUBSTRING(col FROM 3) FROM t"); + } + + public void testSubstringFromFor() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT SUBSTRING('asdf' FROM 1 FOR 2) FROM dual"); + } public void testIssue563MultiSubJoin() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SELECT c FROM ((SELECT a FROM t) JOIN (SELECT b FROM t2) ON a = B JOIN (SELECT c FROM t3) ON b = c)"); diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java index f35434001..8896cbcfa 100644 --- a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java +++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java @@ -22,6 +22,7 @@ import net.sf.jsqlparser.statement.upsert.Upsert; import net.sf.jsqlparser.test.TestException; import net.sf.jsqlparser.test.simpleparsing.CCJSqlParserManagerTest; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import org.junit.Test; @@ -495,4 +496,27 @@ public void testSelectHavingSubquery() throws Exception { assertTrue(tableList.contains("TABLE1")); assertTrue(tableList.contains("TABLE2")); } + + @Test + public void testSubstringFromFor() throws JSQLParserException { + String sql = "select substring((select str from t where id = 1) from (select start_pos from u where id = 1) for (select len from v where id = 1))"; + Statement select = CCJSqlParserUtil.parse(sql); + TablesNamesFinder tablesNamesFinder = new TablesNamesFinder(); + List tableList = tablesNamesFinder.getTableList(select); + assertThat(tableList, containsInAnyOrder("t", "u", "v")); + } + + @Test + public void testSubstringFrom() throws JSQLParserException { + String sql = "select substring((select str from t where id = 1) from (select start_pos from u where id = 1))"; + Statement select = null; + try { + select = CCJSqlParserUtil.parse(sql); + } catch (NullPointerException e) { + fail("Tried to visit an optional for expression?"); + } + TablesNamesFinder tablesNamesFinder = new TablesNamesFinder(); + List tableList = tablesNamesFinder.getTableList(select); + assertThat(tableList, containsInAnyOrder("t", "u")); + } }