You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@shardingsphere.apache.org by "makssent (via GitHub)" <gi...@apache.org> on 2026/10/29 17:21:54 UTC
[I] Firebird Proxy sends batch parameter version error arguments as strings [shardingsphere]
makssent opened a new issue, #40418:
URL: https://github.com/apache/shardingsphere/issues/40418
### Before reporting a bug
- [x] I have searched open and closed [GitHub issues](https://github.com/apache/shardingsphere/issues).
- [x] I have read the [ShardingSphere documentation](https://shardingsphere.apache.org/document/current/en/overview).
### ShardingSphere version
master, commit [5bed7f8c79cb09c56927880a14876fb25e9d5929](https://github.com/apache/shardingsphere/commit/5bed7f8c79cb09c56927880a14876fb25e9d5929)
### ShardingSphere project
ShardingSphere-Proxy
### Expected behavior
The proxy should return numeric status vector arguments for the received and expected batch parameter block versions, as the Firebird server does.
### Actual behavior
When a client creates a batch with parameter block version 2, the proxy returns the correct error but encodes both version arguments as strings.
| Step | Firebird server | Proxy |
|---|---|---|
| Create a batch with parameter block version 2 | `isc_batch_param_version`, `isc_arg_number 2`, `isc_arg_number 1` | `isc_batch_param_version`, `isc_arg_string "2"`, `isc_arg_string "1"` |
| Create a batch with version 1 and synchronize it | Succeeds | Succeeds |
| Release the batch and commit | Succeeds | Succeeds |
Jaybird displays the same message on both sides, but applications inspecting the status vector receive different argument types.
### Reason analysis
The Firebird server checks the version in [`batch_create`](https://github.com/FirebirdSQL/firebird/blob/v5.0.4/src/remote/server/server.cpp#L3625-L3628) and adds the versions as numeric arguments. The proxy's [`FirebirdDialectExceptionMapper`](https://github.com/apache/shardingsphere/blob/5bed7f8c79cb09c56927880a14876fb25e9d5929/database/exception/dialect/firebird/src/main/java/org/apache/shardingsphere/database/exception/firebird/mapper/FirebirdDialectExceptionMapper.java#L76-L78) converts the exception to an SQL exception; [`FirebirdErrorSegmentResolver`](https://github.com/apache/shardingsphere/blob/5bed7f8c79cb09c56927880a14876fb25e9d5929/database/protocol/dialect/firebird/src/main/java/org/apache/shardingsphere/database/protocol/firebird/err/FirebirdErrorSegmentResolver.java#L61-L74) recovers message parameters as strings, which [`FirebirdStatusVector`](https://github.com/apache/shardingsphere/blob/5bed7f8c79cb09c56927880a14876fb25e9d5929/database/protocol/dialect/firebird/sr
c/main/java/org/apache/shardingsphere/database/protocol/firebird/err/FirebirdStatusVector.java#L55-L62) writes as string arguments.
The basic batch creation operation is tracked as implemented in [#39941](https://github.com/apache/shardingsphere/issues/39941); this issue describes the status vector argument types.
### Steps to reproduce
#### Environment
- Firebird 5 compatible server.
- Client: Jaybird 6.0.7.
#### Steps
1. Create the table on the server:
```sql
CREATE TABLE T_PLAIN (ID INTEGER NOT NULL PRIMARY KEY, NAME VARCHAR(20));
```
2. Start ShardingSphere-Proxy with this configuration:
```yaml
# global.yaml
authority:
users:
- user: root@%
password: root
privilege:
type: ALL_PERMITTED
props:
proxy-frontend-database-protocol-type: Firebird
```
```yaml
# database-firebird.yaml
databaseName: firebird_db
dataSources:
ds_0:
url: jdbc:firebirdsql://<host>/<database>
username: <user>
password: <password>
rules:
- !SINGLE
tables:
- "*.*"
```
Set the URLs and credentials in the example to the corresponding server and proxy values.
3. Run the example once with `server` as its argument and once without it. Compare the types in the first status vector and the remaining operations.
### Example code
```java
import org.firebirdsql.gds.ISCConstants;
import org.firebirdsql.gds.JaybirdErrorCodes;
import org.firebirdsql.gds.TransactionParameterBuffer;
import org.firebirdsql.gds.impl.wire.XdrInputStream;
import org.firebirdsql.gds.impl.wire.XdrOutputStream;
import org.firebirdsql.gds.ng.FbExceptionBuilder;
import org.firebirdsql.gds.ng.FbStatement;
import org.firebirdsql.gds.ng.FbTransaction;
import org.firebirdsql.gds.ng.LockCloseable;
import org.firebirdsql.gds.ng.fields.RowValue;
import org.firebirdsql.gds.ng.wire.FbWireDatabase;
import org.firebirdsql.gds.ng.wire.TransmitAction;
import org.firebirdsql.jaybird.fb.constants.TpbItems;
import org.firebirdsql.jdbc.FirebirdConnection;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import java.util.Properties;
public class BatchParamVersion {
static final String SERVER_URL = "jdbc:firebirdsql://<server-host>/<database>";
static final String PROXY_URL = "jdbc:firebirdsql://<proxy-host>:<proxy-port>/firebird_db";
static final String BLR_INT = "05020400020008000700ff4c";
static final String PB = "01020400000001000000";
static boolean closed;
public static void main(String[] args) throws Exception {
java.util.logging.LogManager.getLogManager().reset();
boolean server = 0 < args.length && "server".equals(args[0]);
Properties props = new Properties();
props.setProperty("user", server ? "<server-user>" : "root");
props.setProperty("password", server ? "<server-password>" : "root");
props.setProperty("encoding", "UTF8");
props.setProperty("wireCrypt", "DISABLED");
try (Connection connection = DriverManager.getConnection(server ? SERVER_URL : PROXY_URL, props)) {
FbWireDatabase db = (FbWireDatabase) connection.unwrap(FirebirdConnection.class).getFbDatabase();
run(connection, db);
} catch (SQLException | IOException ex) {
System.out.println("connection -> " + error(ex));
}
}
// Prints the status vector for version 2, then creates and releases a valid batch.
// The server prints number 2 and number 1; the proxy prints string arguments.
static void run(Connection connection, FbWireDatabase db) throws Exception {
FbTransaction tx = db.startTransaction(tpb(db));
FbStatement statement = db.createStatement(tx);
statement.prepare("INSERT INTO T_PLAIN(ID) VALUES (?)");
int h = statement.getHandle();
send(db, out -> {
batchCreate(out, h, BLR_INT, 6, "02020400000001000000");
batchCreate(out, h, BLR_INT, 6, PB);
sync(out);
});
System.out.println("op_batch_create (batch parameter block version 2) -> " + read(db));
System.out.println("op_batch_create (parameter block version 1) -> " + read(db));
System.out.println("op_batch_sync -> " + read(db));
send(db, out -> {
handleOp(out, 102, h); // op_batch_rls
sync(out);
});
System.out.println("op_batch_rls (release batch) -> " + read(db));
System.out.println("op_batch_sync -> " + read(db));
step("close statement", statement::close);
step("COMMIT", tx::commit);
}
static TransactionParameterBuffer tpb(FbWireDatabase db) {
TransactionParameterBuffer result = db.createTransactionParameterBuffer();
result.addArgument(TpbItems.isc_tpb_read_committed);
result.addArgument(TpbItems.isc_tpb_rec_version);
result.addArgument(TpbItems.isc_tpb_write);
result.addArgument(TpbItems.isc_tpb_nowait);
return result;
}
static void execute(Connection connection, String sql) throws SQLException {
try (Statement statement = connection.createStatement()) {
statement.executeUpdate(sql);
}
}
static void send(FbWireDatabase db, TransmitAction action) throws SQLException, IOException {
if (closed) {
throw new EOFException("connection closed");
}
try (LockCloseable ignored = db.withLock()) {
db.completeDeferredActions();
db.getXdrStreamAccess().withTransmitLock(out -> {
action.transmit(out);
out.flush();
});
}
}
interface Action {
void run() throws Exception;
}
static void step(String label, Action action) {
try {
action.run();
System.out.println(label + " -> success");
} catch (Exception ex) {
System.out.println(label + " -> " + error(ex));
}
}
record Response(int op, int object, long blobId, byte[] data, String text) {
@Override
public String toString() {
return text;
}
}
static Response read(FbWireDatabase db) throws SQLException, IOException {
if (closed) {
return new Response(-1, 0, 0, new byte[0], "no response, connection closed");
}
try {
XdrInputStream in = db.getXdrStreamAccess().getXdrIn();
int op;
do {
op = in.readInt();
} while (op == 71); // op_dummy
if (op == 103) { // op_batch_cs
return new Response(op, 0, 0, new byte[0], batchCompletion(in));
}
if (op != 9) {
return new Response(op, 0, 0, new byte[0], "packet " + op + " instead of op_response");
}
int object = in.readInt();
long blobId = in.readLong();
byte[] data = in.readBuffer();
return new Response(op, object, blobId, data, status(in));
} catch (EOFException | SocketException ex) {
closed = true;
return new Response(-1, 0, 0, new byte[0], "no response, connection closed");
}
}
static String batchCompletion(XdrInputStream in) throws IOException {
in.readInt(); // p_batch_statement
int records = in.readInt();
int updates = in.readInt();
int vectors = in.readInt();
int errors = in.readInt();
List<String> counts = new ArrayList<>();
for (int i = 0; i < updates; i++) {
counts.add(String.valueOf(in.readInt()));
}
StringBuilder result = new StringBuilder("op_batch_cs: records " + records + ", counts " + compact(counts) + ", vectors " + vectors + ", errors without vector " + errors);
int first = -1;
int last = -1;
String previous = null;
for (int i = 0; i < vectors; i++) {
int record = in.readInt();
String text = status(in);
if (text.equals(previous) && record == last + 1) {
last = record;
continue;
}
appendVectors(result, first, last, previous);
first = record;
last = record;
previous = text;
}
appendVectors(result, first, last, previous);
List<String> plain = new ArrayList<>();
for (int i = 0; i < errors; i++) {
plain.add(String.valueOf(in.readInt()));
}
if (!plain.isEmpty()) {
result.append("; without vector: records ").append(compact(plain));
}
return result.toString();
}
static void appendVectors(StringBuilder result, int first, int last, String text) {
if (text != null) {
result.append(first == last ? "; record " + first : "; records " + first + "-" + last).append(": ").append(text);
}
}
static String compact(List<String> values) {
List<String> result = new ArrayList<>();
for (int i = 0; i < values.size(); ) {
int j = i;
while (j < values.size() && values.get(j).equals(values.get(i))) {
j++;
}
result.add(j - i > 2 ? values.get(i) + " ×" + (j - i) : String.join(", ", values.subList(i, j)));
i = j;
}
return "[" + String.join(", ", result) + "]";
}
static String status(XdrInputStream in) throws IOException {
FbExceptionBuilder builder = new FbExceptionBuilder();
List<String> codes = new ArrayList<>();
List<String> items = new ArrayList<>();
for (int arg = in.readInt(); arg != 0; arg = in.readInt()) { // isc_arg_end
switch (arg) {
case 1 -> {
int code = in.readInt();
if (code != 0) {
builder.exception(code);
codes.add(code + " " + name(code));
items.add("gds " + code);
}
}
case 18 -> { // isc_arg_warning
int code = in.readInt();
if (code != 0) {
builder.warning(code);
codes.add("warning " + code + " " + name(code));
items.add("warning " + code);
}
}
case 4 -> { // isc_arg_number
int value = in.readInt();
builder.messageParameter(value);
items.add("number " + value);
}
case 19 -> { // isc_arg_sql_state
String value = new String(in.readBuffer(), StandardCharsets.UTF_8);
builder.sqlState(value);
items.add("sqlstate " + value);
}
default -> {
String value = new String(in.readBuffer(), StandardCharsets.UTF_8);
builder.messageParameter(value);
items.add("string '" + value.replace("\n", "\\n") + "'");
}
}
}
if (codes.isEmpty()) {
return "success";
}
String message = String.valueOf(builder.toFlatSQLException().getMessage());
int end = message.indexOf(" [SQLState:");
return "error " + String.join(", ", codes) + " «" + (end < 0 ? message : message.substring(0, end)).replace("\n", " ") + "» " + items;
}
static String error(Exception ex) {
if (ex instanceof EOFException || ex instanceof SocketException || ex.getCause() instanceof EOFException || ex.getCause() instanceof SocketException) {
return "connection closed";
}
if (ex instanceof SQLException sql) {
String message = String.valueOf(sql.getMessage()).split("\n")[0];
int end = message.indexOf(" [SQLState:");
String name = name(sql.getErrorCode());
return "error " + sql.getErrorCode() + (name.isEmpty() ? "" : " " + name) + " «" + (end < 0 ? message : message.substring(0, end)) + "»";
}
return "error " + ex;
}
static String name(int code) {
if (code == 0) {
return "";
}
for (Class<?> each : new Class<?>[]{ISCConstants.class, JaybirdErrorCodes.class}) {
for (Field field : each.getFields()) {
try {
if (field.getType() == int.class && field.getInt(null) == code && (field.getName().startsWith("isc_") || field.getName().startsWith("jb_"))) {
return field.getName();
}
} catch (IllegalAccessException ignored) {
}
}
}
return "(unknown name)";
}
static byte[] xdr(Object... values) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
for (Object each : values) {
byte[] bytes;
if (each instanceof Integer value) {
bytes = new byte[]{(byte) (value >>> 24), (byte) (value >>> 16), (byte) (value >>> 8), (byte) (int) value};
} else if (each instanceof Long value) {
bytes = new byte[8];
for (int i = 0; i < 8; i++) {
bytes[i] = (byte) (value >>> (56 - 8 * i));
}
} else if (each instanceof String value) {
byte[] text = value.getBytes(StandardCharsets.UTF_8);
result.writeBytes(xdr(text.length));
bytes = text;
} else {
bytes = (byte[]) each;
}
result.writeBytes(bytes);
result.writeBytes(new byte[(4 - bytes.length % 4) % 4]);
}
return result.toByteArray();
}
static void batchCreate(XdrOutputStream out, int h, String blr, int messageLength, String pb) throws IOException {
out.writeInt(99); // op_batch_create
out.writeInt(h);
out.writeBuffer(HexFormat.of().parseHex(blr));
out.writeInt(messageLength);
out.writeBuffer(HexFormat.of().parseHex(pb));
}
static void batchMsg(XdrOutputStream out, int h, int count, byte[] messages) throws IOException {
out.writeInt(100); // op_batch_msg
out.writeInt(h);
out.writeInt(count);
out.write(messages, 0, messages.length);
}
static void batchExec(XdrOutputStream out, int h, int transaction) throws IOException {
out.writeInt(101); // op_batch_exec
out.writeInt(h);
out.writeInt(transaction);
}
static void handleOp(XdrOutputStream out, int op, int h) throws IOException {
out.writeInt(op);
out.writeInt(h);
}
static void sync(XdrOutputStream out) throws IOException {
out.writeInt(110); // op_batch_sync
}
}
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@shardingsphere.apache.org.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [I] Firebird Proxy sends batch parameter version error arguments as strings [shardingsphere]
Posted by "schizophrenicmaniac (via GitHub) via notifications" <no...@shardingsphere.apache.org>.
schizophrenicmaniac commented on issue #40418:
URL: https://github.com/apache/shardingsphere/issues/40418#issuecomment-6594238975
Hi, I'd like to work on this. I plan to keep the typed arguments from FirebirdDialectExceptionMapper so that FirebirdStatusVector writes integer arguments like the batch versions as isc_arg_number instead of isc_arg_string, with unit tests for both. I can open a PR within 3 days.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@shardingsphere.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org