Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 ")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is not using upper case anymore?

Copy link
Collaborator Author

@robfrank robfrank Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all the if should use the uppercase to check the command and the pass the original query to executor.

Scenario:

  • command: SeT ....
  • check if SET is present on original query: false
  • check if SET is present on uppercase query: true

setConfiguration(query.query);
resultSet = new IteratorResultSet(createResultSet("STATUS", "Setting ignored").iterator());
} else if (query.query.equals("SELECT VERSION()"))
Expand Down Expand Up @@ -479,7 +479,7 @@ private List<Result> 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);
Expand Down Expand Up @@ -1070,25 +1070,38 @@ 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();

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) {
Expand Down
21 changes: 20 additions & 1 deletion postgresw/src/main/java/com/arcadedb/postgres/PostgresType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,6 +69,10 @@ public enum PostgresType {
private static final Map<Integer, PostgresType> 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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
104 changes: 104 additions & 0 deletions postgresw/src/test/java/com/arcadedb/postgres/PostgresWJdbcIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading