diff --git a/postgresw/src/main/java/com/arcadedb/postgres/PostgresNetworkExecutor.java b/postgresw/src/main/java/com/arcadedb/postgres/PostgresNetworkExecutor.java index 0ae4fc10f2..73f3705392 100755 --- a/postgresw/src/main/java/com/arcadedb/postgres/PostgresNetworkExecutor.java +++ b/postgresw/src/main/java/com/arcadedb/postgres/PostgresNetworkExecutor.java @@ -414,7 +414,7 @@ private void queryCommand() { LogManager.instance().log(this, Level.INFO, "PSQL: query -> %s ", query); final ResultSet resultSet; - if (query.query.startsWith("SET ")) { + if (query.query.toUpperCase(Locale.ENGLISH).startsWith("SET ")) { setConfiguration(query.query); resultSet = new IteratorResultSet(createResultSet("STATUS", "Setting ignored").iterator()); } else if (query.query.equals("SELECT VERSION()")) @@ -479,7 +479,7 @@ private List browseAndCacheResultSet(final ResultSet resultSet, final in private Object[] getParams(PostgresPortal portal) { Object[] parameters = portal.parameterValues != null ? portal.parameterValues.toArray() : new Object[0]; - if (portal.language.equals("cypher")) { + if (portal.language.equals("cypher") || portal.language.equals("opencypher")) { Object[] parametersCypher = new Object[parameters.length * 2]; for (int i = 0; i < parameters.length; i++) { parametersCypher[i * 2] = "" + (i + 1); @@ -1070,10 +1070,21 @@ private void parseCommand() { } private void setConfiguration(final String query) { - final String q = query.substring("SET ".length()); + final int setLength = "SET ".length(); + // Use original query to preserve case of values + final String q = query.substring(setLength); + + // Try to split by either '=' or ' TO ' (case-insensitive) String[] parts = q.split("="); - if (parts.length < 2) - parts = q.split(" TO "); + if (parts.length < 2) { + // Try case-insensitive split for " TO " + parts = q.split("(?i)\\s+TO\\s+"); + } + + if (parts.length < 2) { + LogManager.instance().log(this, Level.WARNING, "Invalid SET command format: %s", query); + return; + } parts[0] = parts[0].trim(); parts[1] = parts[1].trim(); @@ -1081,14 +1092,16 @@ private void setConfiguration(final String query) { if (parts[1].startsWith("'") || parts[1].startsWith("\"")) parts[1] = parts[1].substring(1, parts[1].length() - 1); - if (parts[0].equals("datestyle")) { - if (parts[1].equals("ISO")) + // Use case-insensitive comparison for parameter names + final String paramName = parts[0].toLowerCase(Locale.ENGLISH); + if (paramName.equals("datestyle")) { + if (parts[1].equalsIgnoreCase("ISO")) database.getSchema().setDateTimeFormat(DateUtils.DATE_TIME_ISO_8601_FORMAT); else LogManager.instance().log(this, Level.INFO, "datestyle '%s' not supported", parts[1]); } - connectionProperties.put(parts[0], parts[1]); + connectionProperties.put(paramName, parts[1]); } private void setEmptyResultSet(final PostgresPortal portal) { diff --git a/postgresw/src/main/java/com/arcadedb/postgres/PostgresType.java b/postgresw/src/main/java/com/arcadedb/postgres/PostgresType.java index 7873c84248..1e2b939938 100644 --- a/postgresw/src/main/java/com/arcadedb/postgres/PostgresType.java +++ b/postgresw/src/main/java/com/arcadedb/postgres/PostgresType.java @@ -24,9 +24,12 @@ import com.arcadedb.database.Record; import com.arcadedb.query.sql.executor.Result; import com.arcadedb.serializer.json.JSONObject; +import com.arcadedb.utility.DateUtils; import java.nio.ByteBuffer; import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -66,6 +69,10 @@ public enum PostgresType { private static final Map CODE_MAP = Arrays.stream(values()) .collect(Collectors.toMap(type -> type.code, type -> type)); + // PostgreSQL-compatible datetime format (ISO 8601 without 'T' separator) + private static final String POSTGRES_TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss.SSSSSS"; + private static final DateTimeFormatter POSTGRES_DATETIME_FORMATTER = DateTimeFormatter.ofPattern(POSTGRES_TIMESTAMP_FORMAT); + public final int code; public final Class cls; public final int size; @@ -251,6 +258,13 @@ public void serializeAsText(final PostgresType pgType, final Binary typeBuffer, // Handle primitive arrays by converting them to Collections Collection collection = convertPrimitiveArrayToCollection(value); serializedValue = serializeArrayToString(collection, pgType); + } else if (value instanceof Date date) { + // Format Date as PostgreSQL-compatible timestamp + LocalDateTime ldt = LocalDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC); + serializedValue = ldt.format(POSTGRES_DATETIME_FORMATTER); + } else if (value instanceof LocalDateTime ldt) { + // Format LocalDateTime as PostgreSQL-compatible timestamp + serializedValue = ldt.format(POSTGRES_DATETIME_FORMATTER); } else if (value instanceof JSONObject json) { serializedValue = json.toString(); } else if (value instanceof Map map) { @@ -301,7 +315,12 @@ private String serializeArrayToString(Collection collection, PostgresType pgT } else if (element instanceof Character) { sb.append("'").append(element).append("'"); } else if (element instanceof Date date) { - sb.append(date.getTime()); + // Format Date as PostgreSQL-compatible timestamp in arrays + LocalDateTime ldt = LocalDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC); + sb.append("\"").append(ldt.format(POSTGRES_DATETIME_FORMATTER)).append("\""); + } else if (element instanceof LocalDateTime ldt) { + // Format LocalDateTime as PostgreSQL-compatible timestamp in arrays + sb.append("\"").append(ldt.format(POSTGRES_DATETIME_FORMATTER)).append("\""); } else if (element instanceof Binary binary) { sb.append(binary.getString()); } else if (element instanceof byte[] bytes) { diff --git a/postgresw/src/test/java/com/arcadedb/postgres/PostgresWJdbcIT.java b/postgresw/src/test/java/com/arcadedb/postgres/PostgresWJdbcIT.java index 21287d9042..5f906e7bb7 100644 --- a/postgresw/src/test/java/com/arcadedb/postgres/PostgresWJdbcIT.java +++ b/postgresw/src/test/java/com/arcadedb/postgres/PostgresWJdbcIT.java @@ -372,6 +372,110 @@ void isoDateFormat() throws Exception { } } + /** + * Test for issue #1605: PostgreSQL DATETIME serialization should produce ISO 8601 format + * that is compatible with PostgreSQL clients (especially node-postgres library). + */ + @Test + void dateTimeSerializationFormat() throws Exception { + try (final Connection conn = getConnection()) { + conn.setAutoCommit(false); + try (var st = conn.createStatement()) { + st.execute("CREATE VERTEX TYPE TestDateTime IF NOT EXISTS"); + st.execute("CREATE PROPERTY TestDateTime.created IF NOT EXISTS DATETIME"); + + // Insert a specific datetime value + st.execute("CREATE VERTEX TestDateTime SET name = 'test1', created = '2024-05-19 17:05:11'"); + + // Query the datetime value + ResultSet rs = st.executeQuery("SELECT created FROM TestDateTime WHERE name = 'test1'"); + + assertThat(rs.next()).isTrue(); + + // Verify the timestamp is not null (would be null if pg driver can't parse the format) + java.sql.Timestamp timestamp = rs.getTimestamp("created"); + assertThat(timestamp).isNotNull(); + + // Verify the value is correct + assertThat(timestamp.toString()).startsWith("2024-05-19 17:05:11"); + + rs.close(); + } + } + } + + /** + * Test for issue #1605: SET datestyle command should be case-insensitive + */ + @Test + void setDateStyleCaseInsensitive() throws Exception { + try (final Connection conn = getConnection()) { + try (var st = conn.createStatement()) { + // All these variations should work without errors + st.execute("set datestyle to 'ISO'"); + st.execute("SET DATESTYLE TO 'ISO'"); + st.execute("Set DateStyle To 'ISO'"); + st.execute("SET datestyle = 'ISO'"); + } + } + } + + /** + * Test for issue #1605: SET command should preserve case of values + */ + @Test + void setCommandPreservesCaseOfValues() throws Exception { + try (final Connection conn = getConnection()) { + try (var st = conn.createStatement()) { + // Execute SET with mixed case value + st.execute("SET application_name = 'MyApp'"); + st.execute("SET client_encoding = 'UTF8'"); + + // The SET command should preserve the original case of values + // This test verifies the fix doesn't uppercase values like 'MyApp' to 'MYAPP' + // Note: We can't directly verify the stored value through JDBC, + // but the test ensures no exceptions are thrown and the command is accepted + } + } + } + + /** + * Test for issue #1605: Datetime arrays should be serialized correctly + */ + @Test + void dateTimeArraySerialization() throws Exception { + try (final Connection conn = getConnection()) { + conn.setAutoCommit(false); + try (var st = conn.createStatement()) { + st.execute("CREATE VERTEX TYPE TestDateTimeArray IF NOT EXISTS"); + st.execute("CREATE PROPERTY TestDateTimeArray.dates IF NOT EXISTS LIST"); + + // Insert an array of datetime values + st.execute("CREATE VERTEX TestDateTimeArray SET name = 'test1', dates = ['2024-05-19 17:05:11', '2024-05-20 18:06:12']"); + + // Query the datetime array + ResultSet rs = st.executeQuery("SELECT dates FROM TestDateTimeArray WHERE name = 'test1'"); + + assertThat(rs.next()).isTrue(); + + // Verify the array is not null + Array datesArray = rs.getArray("dates"); + assertThat(datesArray).isNotNull(); + + // Get array elements + Object[] dates = (Object[]) datesArray.getArray(); + assertThat(dates).isNotNull(); + assertThat(dates).hasSize(2); + + // Verify dates are strings in correct format (PostgreSQL JDBC driver will parse them) + assertThat(dates[0]).isNotNull(); + assertThat(dates[1]).isNotNull(); + + rs.close(); + } + } + } + @Test @Disabled void waitForConnectionFromExternal() throws Exception {