From fea51ec6367e30c56ccf27c2c906708f1456ac7b Mon Sep 17 00:00:00 2001 From: simpleqt <2015900818@qq.com> Date: Thu, 5 Nov 2026 03:26:58 +0800 Subject: [PATCH] fix: keep a dot in front of a number in an unsupported statement captureRest() collapsed a dot into the token in front of it whenever the previous token ended with a dot or the current token was a dot, without looking at what followed. A dot which starts a number was therefore glued to its neighbour, and the captured token stream no longer read back the same way once it was rendered and parsed again: REPLACE( . 5 captured as REPLACE ( .5 REPLACE( INTO t VALUES (1, . 5) REPLACE ( INTO t VALUES ( 1 ,.5 ) REPLACE( x . 1 . 2 captured as REPLACE ( x.1.2 toString() of such a statement is not a fixpoint, and the token stream it prints is not what the input contained. The dot is now only used to collapse a qualified name when it is a qualifier. A dot which is followed by a number, and a number in front of a dot, keep the tokens apart. A qualified name still collapses: REPLACE( t . a . b captured as REPLACE ( t.a.b REPLACE( a . b captured as REPLACE ( a.b --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 59 ++++++++++++++++++- .../statement/UnsupportedStatementTest.java | 41 +++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 087c530b4..8f1db4370 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -25128,7 +25128,8 @@ List captureRest() { int l = tokens.size(); if( tok.kind == EOF || tok.kind == ST_SEMICOLON ) { break; - } else if ( l>0 && ( tok.image.equals(".") || tokens.get(l-1).endsWith(".")) ) { + } else if ( l>0 && !joinsNumberPart(tok) && joinsDot(tok, l) ) { + // collapse a qualified name such as a . b into the single a.b tokens.set(l-1, tokens.get(l-1) + tok.image); } else { tokens.add(tok.image); @@ -25138,6 +25139,62 @@ List captureRest() { return tokens; } +/** + * Whether a dot which is about to be consumed joins the previous token to the + * one behind it. + * + * @param tok the token which is about to be consumed + * @param size the number of tokens captured so far + * @return whether the dot belongs to the previous token + */ +JAVACODE +boolean joinsDot(Token tok, int size) { + if (".".equals(tok.image)) { + // the dot closes the previous token, unless it opens a number as in a . 5 + return !numberFollows(); + } + return size > 0 && tok.kind != S_DOUBLE && tok.kind != S_LONG + && numberPrecedes(); +} + +/** + * Whether the token behind the dot which is about to be consumed is a number, + * which makes the dot part of the number instead of a qualifier. + * + * @return whether a number follows the dot + */ +JAVACODE +boolean numberFollows() { + Token next = getToken(2); + return next != null && (next.kind == S_LONG || next.kind == S_DOUBLE); +} + +/** + * Whether the previous token ends with a dot, which makes the token about to be + * consumed the part of a qualified name after that dot. + * + * @return whether a dot precedes the token + */ +JAVACODE +boolean numberPrecedes() { + Token previous = getToken(0); + return previous != null && previous.image.endsWith("."); +} + +/** + * Whether a token has to stay separate because a {@code .} in it starts a + * number. The lexer reads {@code .5} and {@code 5.} as single doubles, so + * gluing such a token to its neighbour changes the token stream and the + * captured statement would no longer parse back. + * + * @param tok the token which is about to be consumed + * @return whether the token is a number + */ +JAVACODE +boolean joinsNumberPart(Token tok) { + return tok.kind == S_DOUBLE || tok.kind == S_LONG; +} + /** * Captures opaque routine declarations through their terminating semicolon. * PostgreSQL AS string bodies can use ordinary, escape or dollar quotes. diff --git a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java index 095fb8c0a..ecf862f8d 100644 --- a/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java @@ -126,6 +126,47 @@ public void testCaptureRestIssue1993() throws JSQLParserException { Assertions.assertEquals(4, statements.size()); } + @Test + void testCaptureRestKeepsADotInFrontOfANumber() throws JSQLParserException { + // a dot which starts a number is not a qualifier, so it must not be + // glued to the token in front of it: the captured tokens have to read + // back the same way once they are rendered and parsed again + for (String sqlStr : Arrays.asList("REPLACE( . 5", + "REPLACE( INTO t VALUES (1, . 5)", + "REPLACE( . . 5)", + "REPLACE( 5 . 5)", + "REPLACE( SELECT . 5", + "REPLACE( x . 1 . 2")) { + Statement statement = CCJSqlParserUtil.parse(sqlStr, + parser -> parser.withUnsupportedStatements(true)); + assertInstanceOf(UnsupportedStatement.class, statement); + + assertEquals(sqlStr.replaceAll("\\s+", ""), statement.toString().replaceAll("\\s+", "")); + + String rendered = statement.toString(); + assertEquals(rendered, CCJSqlParserUtil.parse(rendered, + parser -> parser.withUnsupportedStatements(true)).toString()); + } + } + + @Test + void testCaptureRestStillCollapsesAQualifiedName() throws JSQLParserException { + // the dot of a qualified name is a qualifier and is still collapsed + Statement statement = CCJSqlParserUtil.parse("REPLACE( t . a . b", + parser -> parser.withUnsupportedStatements(true)); + assertInstanceOf(UnsupportedStatement.class, statement); + + assertEquals("REPLACE ( t.a.b", statement.toString()); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(statement.toString(), + parser -> parser.withUnsupportedStatements(true)).toString()); + + // and a quoted name still collapses to the single identifier + Statements statements = CCJSqlParserUtil.parseStatements("Select 1; ALTER TABLE \"inter\".\"inter_user_rec\" \n" + + " OWNER TO \"postgres\"; select 2; select 3;", + parser -> parser.withErrorRecovery(false)); + Assertions.assertEquals(4, statements.size()); + } + @Test void testFormerlyUnsupportedAlterIndex() throws JSQLParserException { String sqlStr =