Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support queries with up to 65535 (inclusive) parameters #2525

Merged
merged 1 commit into from Jun 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
### Added

### Fixed
- fix: queries with up to 65535 (inclusive) parameters are supported now (previous limit was 32767) [PR #2525](https://github.com/pgjdbc/pgjdbc/pull/2525)

## [42.3.6] (2022-05-24 08:52:27 -0400)
### Changed
Expand Down
4 changes: 2 additions & 2 deletions pgjdbc/src/main/java/org/postgresql/core/PGStream.java
Expand Up @@ -355,8 +355,8 @@ public void sendInteger4(int val) throws IOException {
* @throws IOException if an I/O error occurs or {@code val} cannot be encoded in 2 bytes
*/
public void sendInteger2(int val) throws IOException {
if (val < Short.MIN_VALUE || val > Short.MAX_VALUE) {
throw new IOException("Tried to send an out-of-range integer as a 2-byte value: " + val);
if (val < 0 || val > 65535) {
throw new IllegalArgumentException("Tried to send an out-of-range integer as a 2-byte unsigned int value: " + val);
}
int2Buf[0] = (byte) (val >>> 8);
int2Buf[1] = (byte) val;
Expand Down
Expand Up @@ -95,11 +95,25 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {

this.preparedQuery = query;
this.preparedParameters = this.preparedQuery.query.createParameterList();
int parameterCount = preparedParameters.getParameterCount();
int maxSupportedParameters = maximumNumberOfParameters();
if (parameterCount > maxSupportedParameters) {
throw new PSQLException(
GT.tr("PreparedStatement can have at most {0} parameters. Please consider using arrays, or splitting the query in several ones, or using COPY. Given query has {0} parameters",
davecramer marked this conversation as resolved.
Show resolved Hide resolved
maxSupportedParameters,
parameterCount),
PSQLState.INVALID_PARAMETER_VALUE);
}

// TODO: this.wantsGeneratedKeysAlways = true;

setPoolable(true); // As per JDBC spec: prepared and callable statements are poolable by
}

final int maximumNumberOfParameters() {
return connection.getPreferQueryMode() == PreferQueryMode.SIMPLE ? Integer.MAX_VALUE : 65535;
}

@Override
public ResultSet executeQuery(String sql) throws SQLException {
throw new PSQLException(
Expand Down Expand Up @@ -1691,7 +1705,7 @@ protected void transformQueriesAndParameters() throws SQLException {
final int highestBlockCount = 128;
final int maxValueBlocks = bindCount == 0 ? 1024 /* if no binds, use 1024 rows */
: Integer.highestOneBit( // deriveForMultiBatch supports powers of two only
Math.min(Math.max(1, (Short.MAX_VALUE - 1) / bindCount), highestBlockCount));
Math.min(Math.max(1, maximumNumberOfParameters() / bindCount), highestBlockCount));
int unprocessedBatchCount = batchParameters.size();
final int fullValueBlocksCount = unprocessedBatchCount / maxValueBlocks;
final int partialValueBlocksCount = Integer.bitCount(unprocessedBatchCount % maxValueBlocks);
Expand Down
Expand Up @@ -386,13 +386,18 @@ public void testReWriteDisabledForPlainBatch() throws Exception {
}

@Test
public void test32000Binds() throws Exception {
testNBinds(32000);
public void test32767Binds() throws Exception {
testNBinds(32767);
}

@Test
public void test17000Binds() throws Exception {
testNBinds(17000);
public void test32768Binds() throws Exception {
testNBinds(32768);
}

@Test
public void test65535Binds() throws Exception {
testNBinds(65535);
}

public void testNBinds(int nBinds) throws Exception {
Expand All @@ -411,11 +416,20 @@ public void testNBinds(int nBinds) throws Exception {
}
pstmt.addBatch();
}
Assert.assertEquals(
"Statement with " + nBinds
+ " binds should not be batched => two executions with exactly one row inserted each",
Arrays.toString(new int[] { 1, 1 }),
Arrays.toString(pstmt.executeBatch()));
if (nBinds * 2 <= 65535) {
Assert.assertEquals(
"Insert with " + nBinds + " binds should be rewritten into multi-value insert"
+ ", so expecting Statement.SUCCESS_NO_INFO == -2",
Arrays.toString(new int[]{Statement.SUCCESS_NO_INFO, Statement.SUCCESS_NO_INFO}),
Arrays.toString(pstmt.executeBatch()));
} else {
Assert.assertEquals(
"Insert with " + nBinds + " binds can't be rewritten into multi-value insert"
+ " since write format allows 65535 binds maximum"
+ ", so expecting batch to be executed as individual statements",
Arrays.toString(new int[]{1, 1}),
Arrays.toString(pstmt.executeBatch()));
}
} catch (BatchUpdateException be) {
SQLException e = be;
while (true) {
Expand Down
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2022, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/

package org.postgresql.test.jdbc42;

import org.postgresql.PGProperty;
import org.postgresql.jdbc.PreferQueryMode;
import org.postgresql.test.jdbc2.BaseTest4;
import org.postgresql.util.PSQLState;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.sql.Array;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@RunWith(Parameterized.class)
public class PreparedStatement64KBindsTest extends BaseTest4 {
private final int numBinds;
private final PreferQueryMode preferQueryMode;
private final BinaryMode binaryMode;

public PreparedStatement64KBindsTest(int numBinds, PreferQueryMode preferQueryMode,
BinaryMode binaryMode) {
this.numBinds = numBinds;
this.preferQueryMode = preferQueryMode;
this.binaryMode = binaryMode;
}

@Parameterized.Parameters(name = "numBinds={0}, preferQueryMode={1}, binaryMode={2}}")
public static Iterable<Object[]> data() {
Collection<Object[]> ids = new ArrayList<Object[]>();
for (PreferQueryMode preferQueryMode : PreferQueryMode.values()) {
for (BinaryMode binaryMode : BinaryMode.values()) {
for (int numBinds : new int[]{32766, 32767, 32768, 65534, 65535, 65536}) {
ids.add(new Object[]{numBinds, preferQueryMode, binaryMode});
}
}
}
return ids;
}

@Override
protected void updateProperties(Properties props) {
super.updateProperties(props);
PGProperty.PREFER_QUERY_MODE.set(props, preferQueryMode.value());
setBinaryMode(binaryMode);
}

@Test
public void executeWith65535BindsWorks() throws SQLException {
String sql = Collections.nCopies(numBinds, "?").stream()
.collect(Collectors.joining(",", "select ARRAY[", "]"));

try (PreparedStatement ps = con.prepareStatement(sql)) {
for (int i = 1; i <= numBinds; i++) {
ps.setString(i, "v" + i);
}
String expected = Arrays.toString(
IntStream.rangeClosed(1, numBinds)
.mapToObj(i -> "v" + i).toArray()
);

try (ResultSet rs = ps.executeQuery()) {
rs.next();
Array res = rs.getArray(1);
Object[] elements = (Object[]) res.getArray();
String actual = Arrays.toString(elements);

if (preferQueryMode == PreferQueryMode.SIMPLE || numBinds <= 65535) {
Assert.assertEquals("SELECT query with " + numBinds + " should work", actual, expected);
} else {
Assert.fail("con.prepareStatement(..." + numBinds + " binds) should fail since the wire protocol allows only 65535 parameters");
}
}
} catch (SQLException e) {
if (preferQueryMode != PreferQueryMode.SIMPLE && numBinds > 65535) {
Assert.assertEquals(
"con.prepareStatement(..." + numBinds + " binds) should fail since the wire protocol allows only 65535 parameters. SQL State is ",
PSQLState.INVALID_PARAMETER_VALUE.getState(),
e.getSQLState()
);
} else {
throw e;
}
}
}
}