Skip to content

fix: keep a dot in front of a number in an unsupported statement - #2895

Open
simpleqt wants to merge 1 commit into
JSQLParser:masterfrom
simpleqt:fix/capture-rest-dot-before-number
Open

simpleqt wants to merge 1 commit into
JSQLParser:masterfrom
simpleqt:fix/capture-rest-dot-before-number

Conversation

@simpleqt

@simpleqt simpleqt commented Nov 4, 2026

Copy link
Copy Markdown

captureRest() collapses a dot into the token in front of it whenever the previous token ends with a dot or the token is a dot, without looking at what follows. A dot which starts a number is therefore glued to its neighbour, and the captured token stream no longer reads back the same way once the statement is rendered and parsed again.

CCJSqlParserUtil.parse("REPLACE( . 5").toString();                      // REPLACE (.5
CCJSqlParserUtil.parse("REPLACE( . 5").toString()                        // -> REPLACE ( .5
CCJSqlParserUtil.parse("REPLACE( INTO t VALUES (1, . 5)").toString();    // REPLACE ( INTO t VALUES ( 1 ,.5 )
CCJSqlParserUtil.parse("REPLACE( x . 1 . 2").toString();                 // REPLACE ( x.1.2  -> x.1.2 is three tokens

The lexer reads .5 and 5. as single doubles, so joining a dot in that position changes the token stream that the statement was built from.

Fix

captureRest() now only uses a dot to collapse a qualified name when the dot is a qualifier (src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt). A dot which is followed by a number is kept as its own token, and a number in front of a dot is not extended:

REPLACE( . 5                        -> REPLACE ( . 5
REPLACE( INTO t VALUES (1, . 5)     -> REPLACE ( INTO t VALUES ( 1 , . 5 )
REPLACE( x . 1 . 2                  -> REPLACE ( x . 1 . 2
REPLACE( t . a . b                  -> REPLACE ( t.a.b      (qualified name still collapses)
REPLACE( a . b                      -> REPLACE ( a.b

Tests

Two tests in UnsupportedStatementTest:

  • testCaptureRestKeepsADotInFrontOfANumber checks that the captured tokens read back the same way, and that the rendered statement parses to itself.
  • testCaptureRestStillCollapsesAQualifiedName pins the qualified name case, including the quoted "inter"."inter_user_rec" from testCaptureRestIssue1993.

On the parent commit the new test fails with

org.opentest4j.AssertionFailedError: expected: <REPLACE (.5> but was: <REPLACE ( .5>

and with the fix the suite passes:

Tests run: 12411, Failures: 0, Errors: 0, Skipped: 25
BUILD SUCCESS

mvn verify was run locally on JDK 17 with -Dtoolchain.jdk.version=[17,26). The change is confined to JAVACODE methods, so no grammar production is altered and no choice conflict is introduced.

Ref #386 and #1993, which are about the same token echo.

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
Copilot AI balanced review requested due to automatic review settings November 4, 2026 19:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@Sinkleberg

Copy link
Copy Markdown

One token-preservation case remains on this head: REPLACE( 5 . a renders as REPLACE ( 5.a, changing the lexer tokens S_LONG(5), '.', identifier(a) into S_DOUBLE(5.), identifier(a). This already happens on the parent too, but the new whitespace-stripped comparison and rendered-string round trip both pass for it; comparing the original and rendered lexer tokens would catch it.

@manticore-projects

Copy link
Copy Markdown
Contributor

I don't see the benefit in adding the complexity. Do you have any practical relevant use-case?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants