pgjdbc / r2dbc-postgresql

Postgresql R2DBC Driver

Home Page:https://r2dbc.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Calling `bind` on a `SET` statement raises a `syntax error`.

k163377 opened this issue · comments

Bug Report

If bind to a SET statement, the placeholder will not be properly processed and will result in a syntax error .

Versions

  • Driver: org.postgresql:postgresql -> 42.3.6
    • I have confirmed that the same results are obtained with org.postgresql:r2dbc-postgresql and io.r2dbc:r2dbc-spi 0.9.1 and 1.0.0.
  • Database: postgresql 13
  • Java: 11
  • OS: mac OS

Current Behavior

Describe the details in Steps to reproduce.

Table schema

I think it can be reproduced without a table.

Steps to reproduce

I have created a test that looks like the following test.
f1 and f2 are the code that reproduces the problem.
It appears to contain a dependency on the SpringFramework, but this dependency can be removed as long as there is a connectable URL.

Input Code
import io.r2dbc.postgresql.PostgresqlConnectionFactory
import io.r2dbc.spi.ConnectionFactories
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactor.awaitSingle
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.r2dbc.R2dbcProperties
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class BindToSetQueryTest @Autowired constructor(r2dbcProperties: R2dbcProperties) {
    val cf = ConnectionFactories.get(r2dbcProperties.url) as PostgresqlConnectionFactory

    // Direct embedding
    @Test
    fun t1() = runTest {
        val con = cf.create().awaitSingle()

        con.createStatement("set app.current_tenant = 'foo'").execute().awaitSingle()
        val appCurrentTenant = con.createStatement("select current_setting('app.current_tenant'::text)")
            .execute()
            .flatMap { it.map { t, _ -> t.get(0) } }
            .awaitSingle()

        assertEquals("foo", appCurrentTenant)
    }

    // Select bound value
    @Test
    fun t2() = runTest {
        val con = cf.create().awaitSingle()

        val boundValue = con.createStatement("select $1").bind(0, "foo")
            .execute()
            .flatMap { it.map { t, _ -> t.get(0) } }
            .awaitSingle()

        assertEquals("foo", boundValue)
    }

    @Test
    fun f1() = runTest {
        val con = cf.create().awaitSingle()

        con.createStatement("set app.current_tenant = $1").bind(0, "foo").execute().awaitSingle()
        val appCurrentTenant = con.createStatement("select current_setting('app.current_tenant'::text)")
            .execute()
            .flatMap { it.map { t, _ -> t.get(0) } }
            .awaitSingle()

        assertEquals("foo", appCurrentTenant)
    }

    @Test
    fun f2() = runTest {
        val con = cf.create().awaitSingle()

        con.createStatement("set app.current_tenant = $1").bind(0, "foo")
            .execute()
            .flatMap { it.map { t, _ -> t.metadata } } // The process by which the query is to be executed.
            .awaitSingle()
    }
}

f1 will give an error unrecognized configuration parameter "app.current_tenant".

Stack trace for f1
unrecognized configuration parameter "app.current_tenant"
io.r2dbc.postgresql.ExceptionFactory$PostgresqlBadGrammarException: [42704] unrecognized configuration parameter "app.current_tenant"
	at app//io.r2dbc.postgresql.ExceptionFactory.createException(ExceptionFactory.java:96)
	at app//io.r2dbc.postgresql.ExceptionFactory.createException(ExceptionFactory.java:65)
	at app//io.r2dbc.postgresql.ExceptionFactory.handleErrorResponse(ExceptionFactory.java:132)
	at app//io.r2dbc.postgresql.PostgresqlResult.lambda$map$2(PostgresqlResult.java:112)
	at app//reactor.core.publisher.FluxHandleFuseable$HandleFuseableSubscriber.onNext(FluxHandleFuseable.java:176)
	at app//reactor.core.publisher.FluxWindowPredicate$WindowFlux.drainRegular(FluxWindowPredicate.java:668)
	at app//reactor.core.publisher.FluxWindowPredicate$WindowFlux.drain(FluxWindowPredicate.java:746)
	at app//reactor.core.publisher.FluxWindowPredicate$WindowFlux.onNext(FluxWindowPredicate.java:788)
	at app//reactor.core.publisher.FluxWindowPredicate$WindowPredicateMain.onNext(FluxWindowPredicate.java:239)
	at app//io.r2dbc.postgresql.util.FluxDiscardOnCancel$FluxDiscardOnCancelSubscriber.onNext(FluxDiscardOnCancel.java:91)
	at app//reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)
	at app//reactor.core.publisher.FluxCreate$BufferAsyncSink.drain(FluxCreate.java:814)
	at app//reactor.core.publisher.FluxCreate$BufferAsyncSink.next(FluxCreate.java:739)
	at app//reactor.core.publisher.FluxCreate$SerializedFluxSink.next(FluxCreate.java:161)
	at app//io.r2dbc.postgresql.client.ReactorNettyClient$Conversation.emit(ReactorNettyClient.java:687)
	at app//io.r2dbc.postgresql.client.ReactorNettyClient$BackendMessageSubscriber.emit(ReactorNettyClient.java:939)
	at app//io.r2dbc.postgresql.client.ReactorNettyClient$BackendMessageSubscriber.onNext(ReactorNettyClient.java:813)
	at app//io.r2dbc.postgresql.client.ReactorNettyClient$BackendMessageSubscriber.onNext(ReactorNettyClient.java:719)
	at app//reactor.core.publisher.FluxHandle$HandleSubscriber.onNext(FluxHandle.java:126)
	at app//reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onNext(FluxPeekFuseable.java:854)
	at app//reactor.core.publisher.FluxMap$MapConditionalSubscriber.onNext(FluxMap.java:224)
	at app//reactor.core.publisher.FluxMap$MapConditionalSubscriber.onNext(FluxMap.java:224)
	at app//reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:279)
	at app//reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:388)
	at app//reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:404)
	at app//reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:93)
	at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at app//io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at app//io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:327)
	at app//io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:299)
	at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at app//io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at app//io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
	at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at app//io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
	at app//io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
	at app//io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722)
	at app//io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658)
	at app//io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584)
	at app//io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496)
	at app//io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
	at app//io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at app//io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base@11.0.16.1/java.lang.Thread.run(Thread.java:829)

f2 will give an error syntax error at or near "$1".

Stack trace for f2
syntax error at or near "$1"
io.r2dbc.postgresql.ExceptionFactory$PostgresqlBadGrammarException: [42601] syntax error at or near "$1"
	at app//io.r2dbc.postgresql.ExceptionFactory.createException(ExceptionFactory.java:96)
	at app//io.r2dbc.postgresql.ExceptionFactory.createException(ExceptionFactory.java:65)
	at app//io.r2dbc.postgresql.ExceptionFactory.handleErrorResponse(ExceptionFactory.java:132)
	at app//reactor.core.publisher.FluxHandleFuseable$HandleFuseableSubscriber.onNext(FluxHandleFuseable.java:176)
	at app//reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337)
	at app//reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)
	at app//reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onNext(FluxPeekFuseable.java:854)
	at app//reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onNext(FluxPeekFuseable.java:854)
	at app//io.r2dbc.postgresql.util.FluxDiscardOnCancel$FluxDiscardOnCancelSubscriber.onNext(FluxDiscardOnCancel.java:91)
	at app//reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onNext(FluxDoFinally.java:113)
	at app//reactor.core.publisher.FluxHandle$HandleSubscriber.onNext(FluxHandle.java:126)
	at app//reactor.core.publisher.FluxCreate$BufferAsyncSink.drain(FluxCreate.java:814)
	at app//reactor.core.publisher.FluxCreate$BufferAsyncSink.next(FluxCreate.java:739)
	at app//reactor.core.publisher.FluxCreate$SerializedFluxSink.next(FluxCreate.java:161)
	at app//io.r2dbc.postgresql.client.ReactorNettyClient$Conversation.emit(ReactorNettyClient.java:687)
	at app//io.r2dbc.postgresql.client.ReactorNettyClient$BackendMessageSubscriber.emit(ReactorNettyClient.java:939)
	at app//io.r2dbc.postgresql.client.ReactorNettyClient$BackendMessageSubscriber.onNext(ReactorNettyClient.java:813)
	at app//io.r2dbc.postgresql.client.ReactorNettyClient$BackendMessageSubscriber.onNext(ReactorNettyClient.java:719)
	at app//reactor.core.publisher.FluxHandle$HandleSubscriber.onNext(FluxHandle.java:126)
	at app//reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onNext(FluxPeekFuseable.java:854)
	at app//reactor.core.publisher.FluxMap$MapConditionalSubscriber.onNext(FluxMap.java:224)
	at app//reactor.core.publisher.FluxMap$MapConditionalSubscriber.onNext(FluxMap.java:224)
	at app//reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:279)
	at app//reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:388)
	at app//reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:404)
	at app//reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:93)
	at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at app//io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at app//io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:327)
	at app//io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:314)
	at app//io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:435)
	at app//io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:279)
	at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at app//io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at app//io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
	at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at app//io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
	at app//io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
	at app//io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722)
	at app//io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658)
	at app//io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584)
	at app//io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496)
	at app//io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
	at app//io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at app//io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base@11.0.16.1/java.lang.Thread.run(Thread.java:829)

Expected behavior/code

As far as the results for t1 and t2 are concerned, it is correct that f1 and f2 are not errors.
Also, for f1, I think it is correct that it should be a syntax error at or near "$1" (although this may be a separate issue).

Possible Solution

The r2dbc-postgresql does not expose the escaping process, and I don't think there is a workaround.
I encountered this problem in the postAllocate process of r2dbc-pool and finally settled on using jOOQ to generate a query with embedded parameters.

Additional context

The r2dbc-postgresql does not expose the escaping process,

R2DBC drivers generally do not rewrite not escape any provided SQL. Any error message concerning syntax or grammar that are reported by your Postgres server are caused by SQL input that isn't working.

I am able to reproduce the exact same result using JDBC:

org.postgresql.util.PSQLException: ERROR: syntax error at or near "$1"
  Position: 26
	at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2676)
	at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2366)
	at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:356)
	at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:496)
	at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:413)
	at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:190)

Code to reproduce:

JdbcOperations ops = …
ops.execute("set app.current_tenant = 'foo'");
ops.update("set app.current_tenant = ?", "foo"); // reproducer fails here