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