diff --git a/xmppserver/src/main/java/org/jivesoftware/database/DbConnectionManager.java b/xmppserver/src/main/java/org/jivesoftware/database/DbConnectionManager.java
index d143f1769e..45f6c34db8 100644
--- a/xmppserver/src/main/java/org/jivesoftware/database/DbConnectionManager.java
+++ b/xmppserver/src/main/java/org/jivesoftware/database/DbConnectionManager.java
@@ -1143,6 +1143,33 @@ public static String getIdentifierQuoteString() {
return identifierQuoteString;
}
+ /**
+ * Returns the keyword or keyword phrase used by the active database to limit result sets.
+ *
+ * Depending on the database, this can be {@link ResultSetLimitKeyword#TOP},
+ * {@link ResultSetLimitKeyword#LIMIT}, or {@link ResultSetLimitKeyword#FETCH_FIRST}.
+ *
+ * @return the result-set limit keyword used by the current database.
+ * @see OF-3277
+ */
+ public static ResultSetLimitKeyword getResultSetLimitKeyword()
+ {
+ return databaseType.getResultSetLimitKeyword();
+ }
+
+ /**
+ * Indicates if the result-set limit keyword is used as a prefix before the selected columns.
+ *
+ * This is true for databases such as SQL Server that use {@code SELECT TOP (...) ...}.
+ *
+ * @return {@code true} if the keyword is prefix-style, otherwise {@code false}.
+ * @see OF-3277
+ */
+ public static boolean isResultSetLimitKeywordPrefix()
+ {
+ return databaseType.isResultSetLimitKeywordPrefix();
+ }
+
public static String getTestSQL(String driver) {
if (driver == null) {
return "select 1";
@@ -1196,5 +1223,58 @@ public String escapeIdentifier(final String keyword) {
return keyword;
}
}
+
+ /**
+ * Returns the keyword or keyword phrase that should be used to limit result sets for this database type.
+ *
+ * @return the result-set limit keyword, or a sensible default when the database type is unknown.
+ * @see OF-3277
+ */
+ public ResultSetLimitKeyword getResultSetLimitKeyword()
+ {
+ return switch (this) {
+ case sqlserver -> ResultSetLimitKeyword.TOP;
+ case oracle, db2 -> ResultSetLimitKeyword.FETCH_FIRST;
+ default -> ResultSetLimitKeyword.LIMIT;
+ };
+ }
+
+ /**
+ * Indicates if the result-set limit keyword is used before the column list in a SELECT statement.
+ *
+ * @return {@code true} when the keyword is prefix-style (for example {@code TOP}), otherwise {@code false}.
+ * @see OF-3277
+ */
+ public boolean isResultSetLimitKeywordPrefix()
+ {
+ return getResultSetLimitKeyword().isPrefix();
+ }
+ }
+
+ /**
+ * Identifies how a database limits result sets.
+ *
+ * @see OF-3277
+ */
+ public enum ResultSetLimitKeyword
+ {
+ TOP(true),
+ FETCH_FIRST(false),
+ LIMIT(false);
+
+ private final boolean prefix;
+
+ ResultSetLimitKeyword(final boolean prefix) {
+ this.prefix = prefix;
+ }
+
+ /**
+ * Indicates if the keyword is used before the selected columns in a {@code SELECT} statement.
+ *
+ * @return {@code true} for prefix-style keywords such as {@code TOP}, otherwise {@code false}.
+ */
+ public boolean isPrefix() {
+ return prefix;
+ }
}
}
diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/DefaultPubSubPersistenceProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/DefaultPubSubPersistenceProvider.java
index 7ca1d05946..8461780658 100644
--- a/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/DefaultPubSubPersistenceProvider.java
+++ b/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/DefaultPubSubPersistenceProvider.java
@@ -1631,21 +1631,26 @@ else if (maxPublished != -1)
{
con = DbConnectionManager.getConnection();
// Get published items of the specified node
- if (DbConnectionManager.getDatabaseType().equals(DbConnectionManager.DatabaseType.sqlserver)) {
- pstmt = con.prepareStatement(LOAD_LAST_ITEMS_TOP);
- } else if (DbConnectionManager.getDatabaseType().equals(DbConnectionManager.DatabaseType.oracle)) {
- pstmt = con.prepareStatement(LOAD_LAST_ITEMS_FETCHFIRST);
- } else {
- pstmt = con.prepareStatement(LOAD_LAST_ITEMS_LIMIT);
+ switch (DbConnectionManager.getDatabaseType().getResultSetLimitKeyword()) {
+ case TOP:
+ pstmt = con.prepareStatement(LOAD_LAST_ITEMS_TOP);
+ break;
+ case FETCH_FIRST:
+ pstmt = con.prepareStatement(LOAD_LAST_ITEMS_FETCHFIRST);
+ break;
+ case LIMIT: // Intended fall-through
+ default:
+ pstmt = con.prepareStatement(LOAD_LAST_ITEMS_LIMIT);
+ break;
}
pstmt.setMaxRows(max);
int paramIndex = 0;
- if (DbConnectionManager.getDatabaseType().equals(DbConnectionManager.DatabaseType.sqlserver)){
+ if (DbConnectionManager.getDatabaseType().isResultSetLimitKeywordPrefix()) {
pstmt.setLong(++paramIndex, max);
}
pstmt.setString(++paramIndex, node.getUniqueIdentifier().getServiceIdentifier().getServiceId());
pstmt.setString(++paramIndex, encodeNodeID(node.getNodeID()));
- if (!DbConnectionManager.getDatabaseType().equals(DbConnectionManager.DatabaseType.sqlserver)){
+ if (!DbConnectionManager.getDatabaseType().isResultSetLimitKeywordPrefix()) {
pstmt.setLong(++paramIndex, max);
}
rs = pstmt.executeQuery();
diff --git a/xmppserver/src/test/java/org/jivesoftware/database/DbConnectionManagerTest.java b/xmppserver/src/test/java/org/jivesoftware/database/DbConnectionManagerTest.java
new file mode 100644
index 0000000000..f1cd2562d5
--- /dev/null
+++ b/xmppserver/src/test/java/org/jivesoftware/database/DbConnectionManagerTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2026 Ignite Realtime Foundation. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.database;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class DbConnectionManagerTest {
+
+ /**
+ * Verifies that SQL Server uses TOP as a prefix-style row limiting keyword.
+ */
+ @Test
+ public void sqlServerUsesTopAsAPrefixKeyword() {
+ assertEquals(DbConnectionManager.ResultSetLimitKeyword.TOP, DbConnectionManager.DatabaseType.sqlserver.getResultSetLimitKeyword(), "SQL Server should map to the TOP limit keyword.");
+ assertTrue(DbConnectionManager.DatabaseType.sqlserver.isResultSetLimitKeywordPrefix(), "TOP should be marked as a prefix-style keyword.");
+ }
+
+ /**
+ * Verifies that Oracle uses FETCH FIRST as a suffix-style row limiting keyword.
+ */
+ @Test
+ public void oracleUsesFetchFirstAsASuffixKeyword() {
+ assertEquals(DbConnectionManager.ResultSetLimitKeyword.FETCH_FIRST, DbConnectionManager.DatabaseType.oracle.getResultSetLimitKeyword(), "Oracle should map to the FETCH_FIRST limit keyword.");
+ assertFalse(DbConnectionManager.DatabaseType.oracle.isResultSetLimitKeywordPrefix(), "FETCH_FIRST should be marked as a suffix-style keyword.");
+ }
+
+ /**
+ * Verifies that DB2 uses FETCH FIRST as a suffix-style row limiting keyword.
+ */
+ @Test
+ public void db2UsesFetchFirstAsASuffixKeyword() {
+ assertEquals(DbConnectionManager.ResultSetLimitKeyword.FETCH_FIRST, DbConnectionManager.DatabaseType.db2.getResultSetLimitKeyword(), "DB2 should map to the FETCH_FIRST limit keyword.");
+ assertFalse(DbConnectionManager.DatabaseType.db2.isResultSetLimitKeywordPrefix(), "DB2 should use a suffix-style limit keyword.");
+ }
+
+ /**
+ * Verifies that common ANSI-style databases map to LIMIT.
+ */
+ @Test
+ public void commonAnsiStyleDatabasesUseLimit() {
+ assertEquals(DbConnectionManager.ResultSetLimitKeyword.LIMIT, DbConnectionManager.DatabaseType.mysql.getResultSetLimitKeyword(), "MySQL should map to the LIMIT keyword.");
+ assertEquals(DbConnectionManager.ResultSetLimitKeyword.LIMIT, DbConnectionManager.DatabaseType.postgresql.getResultSetLimitKeyword(), "PostgreSQL should map to the LIMIT keyword.");
+ assertEquals(DbConnectionManager.ResultSetLimitKeyword.LIMIT, DbConnectionManager.DatabaseType.hsqldb.getResultSetLimitKeyword(), "HSQLDB should map to the LIMIT keyword.");
+ assertEquals(DbConnectionManager.ResultSetLimitKeyword.LIMIT, DbConnectionManager.DatabaseType.unknown.getResultSetLimitKeyword(), "Unknown database types should default to LIMIT.");
+ }
+
+ /**
+ * Verifies prefix-position metadata for all supported row limiting keywords.
+ */
+ @Test
+ public void enumKnowsWhetherItIsPrefixStyle() {
+ assertTrue(DbConnectionManager.ResultSetLimitKeyword.TOP.isPrefix(), "TOP should be treated as a prefix-style keyword.");
+ assertFalse(DbConnectionManager.ResultSetLimitKeyword.FETCH_FIRST.isPrefix(), "FETCH_FIRST should be treated as a suffix-style keyword.");
+ assertFalse(DbConnectionManager.ResultSetLimitKeyword.LIMIT.isPrefix(), "LIMIT should be treated as a suffix-style keyword.");
+ }
+}
+
+