feat(agents): remove legacy Oracle Java agents

This commit is contained in:
t8y2 2026-06-28 16:11:22 +08:00
parent 7486d35a0f
commit 6c232899b4
24 changed files with 66 additions and 2012 deletions

View File

@ -61,20 +61,16 @@ jobs:
echo "Skipping automatic module version bump for migrated first release; versions.json was carried over from dbx-agents."
else
for dir in $(echo "$VERSIONS" | python3 -c "import sys,json; [print(k) for k in json.load(sys.stdin)]"); do
if [ -d "agents/drivers/$dir" ] || [ -d "agents/$dir" ] || [ "$dir" = "common" ]; then
if [ -d "agents/drivers/$dir" ]; then
CHECK_DIR="drivers/$dir"
else
CHECK_DIR="$dir"
fi
EXTRA_CHECK_DIR=""
if [ "$dir" = "oracle" ]; then
EXTRA_CHECK_DIR="drivers/oracle-go"
fi
DIFF_PATHS=("agents/$CHECK_DIR/")
if [ -n "$EXTRA_CHECK_DIR" ]; then
DIFF_PATHS+=("agents/$EXTRA_CHECK_DIR/")
fi
if [ "$dir" = "oracle" ] && [ -d "agents/drivers/oracle-go" ]; then
CHECK_DIR="drivers/oracle-go"
elif [ -d "agents/drivers/$dir" ]; then
CHECK_DIR="drivers/$dir"
elif [ -d "agents/$dir" ] || [ "$dir" = "common" ]; then
CHECK_DIR="$dir"
else
continue
fi
DIFF_PATHS=("agents/$CHECK_DIR/")
# common changes affect all modules
if git diff --quiet "$PREV_TAG"..HEAD -- agents/common/ 2>/dev/null; then
COMMON_CHANGED=false
@ -99,7 +95,6 @@ jobs:
CHANGED=true
echo " $dir: $OLD_VER -> $NEW_VER"
fi
fi
done
fi
@ -290,50 +285,8 @@ jobs:
name: jre-${{ matrix.jre-key }}
path: "dbx-jre-*.tar.gz"
build-jre8:
runs-on: ubuntu-latest
steps:
- name: Download Adoptium JRE 8 for all platforms
shell: bash
run: |
ADOPTIUM="https://api.adoptium.net/v3/binary/latest/8/ga"
declare -A PLATFORMS=(
["linux-x64"]="linux/x64"
["linux-aarch64"]="linux/aarch64"
["macos-x64"]="mac/x64"
["windows-x64"]="windows/x64"
)
for platform in "${!PLATFORMS[@]}"; do
os_arch="${PLATFORMS[$platform]}"
echo "=== Downloading JRE 8 for $platform ($os_arch) ==="
if [[ "$platform" == windows-* ]]; then
curl -L -f -o jre8-$platform.zip "$ADOPTIUM/$os_arch/jre/hotspot/normal/eclipse?project=jdk"
mkdir -p jre8-extract
unzip -q jre8-$platform.zip -d jre8-extract
mv jre8-extract/*/ dbx-jre || mv jre8-extract/* dbx-jre
rm -rf jre8-extract jre8-$platform.zip
else
curl -L -f -o jre8-$platform.tar.gz "$ADOPTIUM/$os_arch/jre/hotspot/normal/eclipse?project=jdk"
mkdir -p dbx-jre
tar xzf jre8-$platform.tar.gz -C dbx-jre --strip-components=1
rm -f jre8-$platform.tar.gz
fi
tar czf dbx-jre-8-$platform.tar.gz dbx-jre
rm -rf dbx-jre
done
if [ -f dbx-jre-8-macos-x64.tar.gz ]; then
cp dbx-jre-8-macos-x64.tar.gz dbx-jre-8-macos-aarch64.tar.gz
fi
ls -lh dbx-jre-8-*.tar.gz
- uses: actions/upload-artifact@v4
with:
name: jre-8
path: "dbx-jre-8-*.tar.gz"
release:
needs: [bump-versions, build-agents, build-oracle-native, build-xugu-native, build-jre, build-jre8]
needs: [bump-versions, build-agents, build-oracle-native, build-xugu-native, build-jre]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -445,13 +398,11 @@ jobs:
detect_jre_key() {
local name="$1"
case "$name" in
oracle-10g) echo "8" ;;
*) echo "21" ;;
esac
}
JRE21_PLATFORMS=$(generate_jre_platforms "21")
JRE8_PLATFORMS=$(generate_jre_platforms "8")
DRIVERS=""
for f in release/dbx-agent-*.jar; do
@ -466,7 +417,7 @@ jobs:
[ -n "$DRIVERS" ] && DRIVERS="${DRIVERS},"$'\n'
DRIVERS="${DRIVERS}$(generate_jar_entry "$name" "$label" "$f" "$jre_key" "$version" "$external_driver" "$native_json")"
done
for name in xugu; do
for name in oracle xugu; do
[ -f "release/dbx-agent-${name}.jar" ] && continue
native_json=$(generate_native_platforms "$name")
[ -z "$native_json" ] && continue
@ -485,12 +436,6 @@ jobs:
"platforms": {
${JRE21_PLATFORMS}
}
},
"8": {
"version": "8u442",
"platforms": {
${JRE8_PLATFORMS}
}
}
},
"drivers": {
@ -517,7 +462,7 @@ jobs:
for f in release/dbx-agent-*.jar; do
DRIVER_NAMES+=("$(basename "$f" .jar | sed 's/dbx-agent-//')")
done
for name in xugu; do
for name in oracle xugu; do
[ -f "release/dbx-agent-${name}.jar" ] && continue
compgen -G "release/dbx-agent-${name}-*" > /dev/null && DRIVER_NAMES+=("$name")
done
@ -525,6 +470,7 @@ jobs:
native_only_label() {
local name="$1"
case "$name" in
oracle) echo "Oracle" ;;
xugu) echo "虚谷 XuguDB" ;;
*) echo "$name" ;;
esac
@ -544,15 +490,14 @@ jobs:
fi
NOTES="${NOTES}### ${label} (${old_ver} → ${new_ver})"$'\n'
if [ -d "agents/drivers/$name" ]; then
if [ "$name" = "oracle" ]; then
LOG_PATH="agents/drivers/oracle-go/"
elif [ -d "agents/drivers/$name" ]; then
LOG_PATH="agents/drivers/$name/"
else
LOG_PATH="agents/$name/"
fi
LOG_PATHS=("$LOG_PATH")
if [ "$name" = "oracle" ]; then
LOG_PATHS+=("agents/drivers/oracle-go/")
fi
while IFS= read -r line; do
[ -n "$line" ] && NOTES="${NOTES}- ${line}"$'\n'
done < <(git log --oneline "$PREV_TAG"..HEAD -- "${LOG_PATHS[@]}" | sed 's/^[0-9a-f]* //')

View File

@ -24,8 +24,6 @@ Each agent runs as a standalone process and communicates with DBX via stdin/stdo
| gbase8a | GBase 8a | External GBase 8a JDBC |
| gbase8s | GBase 8s | External GBase 8s JDBC |
| oracle | Oracle 10g+ | go-ora native agent |
| oracle-legacy | Oracle 11g/12c/18c/19c | ojdbc8 (compatibility fallback) |
| oracle-10g | Oracle 10g | ojdbc6 (compatibility fallback, JRE 8) |
| h2 | H2 | H2 JDBC |
| snowflake | Snowflake | Snowflake JDBC |
| trino | Trino (Presto) | Trino JDBC |
@ -48,11 +46,11 @@ Each agent runs as a standalone process and communicates with DBX via stdin/stdo
## Multi-JRE Support
Most Java agents target JRE 21. Native agents, such as `oracle` and `xugu`, do not require a JRE. Agents that still require legacy Java runtimes (e.g. compatibility fallback `oracle-10g` uses JRE 8) declare their JRE version in the registry. DBX downloads and manages multiple JRE installations automatically.
Most Java agents target JRE 21. Native agents, such as `oracle` and `xugu`, do not require a JRE. DBX downloads and manages the JRE 21 installation automatically for Java agents.
## Build
Requires JDK 8 and 21 (Gradle toolchain auto-downloads if needed).
Requires JDK 21 (Gradle toolchain auto-downloads if needed).
```bash
./gradlew shadowJar
@ -64,7 +62,7 @@ Output JARs are in `drivers/{module}/build/libs/`. Native agents build from `dri
### Local DBX Runtime Test
When changing `agents/drivers/<db_type>/` or shared Java agent protocol code, rebuild the target agent and replace the runtime JAR used by the local DBX app:
When changing a Java agent under `agents/drivers/<db_type>/` or shared Java agent protocol code, rebuild the target agent and replace the runtime JAR used by the local DBX app:
```bash
./gradlew :<db_type>:shadowJar
@ -74,6 +72,8 @@ cp agents/drivers/<db_type>/build/libs/*-all.jar ~/.dbx/agents/drivers/<db_type>
Restart DBX or disconnect and reconnect the database so the new agent process loads the replacement JAR.
Native agents such as `oracle` and `xugu` use the `agent` executable in the driver directory instead of `agent.jar`.
## Development
- Agent authoring guide: [docs/agent-authoring.md](docs/agent-authoring.md)

View File

@ -2,7 +2,7 @@ plugins {
id 'com.gradleup.shadow' version '9.0.0-beta12' apply false
}
def java8Projects = ['common', 'test-support', 'oracle-10g'] as Set
def java8Projects = ['common', 'test-support'] as Set
def infrastructureProjects = ['common', 'test-support'] as Set
def legacyStandaloneProjects = ['mongodb'] as Set
def agentProjects = subprojects.findAll { !infrastructureProjects.contains(it.name) }

View File

@ -182,7 +182,7 @@ The root `build.gradle` convention derives the archive name from the module name
Default to:
```groovy
def java8Projects = ['common', 'test-support', 'oracle-10g'] as Set
def java8Projects = ['common', 'test-support'] as Set
subprojects {
java {
@ -193,7 +193,7 @@ subprojects {
}
```
Most agents use JRE 21. Only use JRE 8 for drivers that require it, such as legacy Oracle 10g support. If an agent needs a special runtime, update the root Gradle convention, the release workflow JRE detection logic, and document the reason in the module.
Most agents use JRE 21. If an agent needs a special runtime, update the root Gradle convention, the release workflow JRE detection logic, and document the reason in the module.
## Tests

View File

@ -113,15 +113,9 @@ Current external-driver agents include BigQuery and SunDB.
## 6. JRE Selection
Most agents are built for the default JRE key `21`, backed by JDK 21 in the release workflow.
Java agents are built for the default JRE key `21`, backed by JDK 21 in the release workflow. Native agents do not require a JRE.
Special case:
- `oracle-10g` uses JRE key `8`.
If another agent needs a different runtime:
- Update the release workflow JRE detection logic.
If another agent needs a different runtime, update the release workflow JRE detection logic.
- Document why in the module or release notes.
- Verify DBX can download the matching runtime artifact.
@ -183,8 +177,7 @@ After the GitHub release finishes:
- Download or inspect `agent-registry.json`.
- Confirm every expected agent appears under `drivers`.
- Confirm labels preserve spaces, for example `Google BigQuery`.
- Confirm `oracle-10g` uses JRE key `8`.
- Confirm other agents use JRE key `21`.
- Confirm Java agents use JRE key `21`.
- Confirm `external_driver_required` is correct.
- Confirm every jar URL, sha256, and size is present.
- Spot-check at least one agent jar manifest:

View File

@ -1,11 +0,0 @@
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.oracle.database.jdbc:ojdbc6:11.2.0.4'
implementation 'com.oracle.database.nls:orai18n:11.2.0.4'
}
tasks.named('shadowJar') {
manifest {
attributes('Agent-Label': 'Oracle 10g', 'Main-Class': 'com.dbx.agent.oracle10g.Oracle10gAgent')
}
}

View File

@ -1,593 +0,0 @@
package com.dbx.agent.oracle10g;
import com.dbx.agent.BaseDatabaseAgent;
import com.dbx.agent.ColumnInfo;
import com.dbx.agent.ConnectParams;
import com.dbx.agent.DatabaseInfo;
import com.dbx.agent.ExecuteQueryOptions;
import com.dbx.agent.ForeignKeyInfo;
import com.dbx.agent.IndexInfo;
import com.dbx.agent.JdbcExecutor;
import com.dbx.agent.JdbcIdentifiers;
import com.dbx.agent.JsonRpcServer;
import com.dbx.agent.ObjectInfo;
import com.dbx.agent.ObjectSource;
import com.dbx.agent.QueryPageOptions;
import com.dbx.agent.QueryPageResult;
import com.dbx.agent.QueryResult;
import com.dbx.agent.TableInfo;
import com.dbx.agent.TriggerInfo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class Oracle10gAgent extends BaseDatabaseAgent {
private static final List<String> SYSTEM_SCHEMAS = Arrays.asList(
"SYS", "SYSTEM", "SYSMAN", "DBSNMP", "SYSBACKUP", "SYSDG", "SYSKM", "OUTLN",
"AUDSYS", "LBACSYS", "DVF", "DVSYS", "APPQOSSYS", "CTXSYS", "MDSYS", "MDDATA",
"ORDSYS", "ORDDATA", "ORDPLUGINS", "XDB", "ANONYMOUS", "DIP", "EXFSYS",
"GSMADMIN_INTERNAL", "GSMCATUSER", "GSMUSER", "OJVMSYS", "OLAPSYS",
"ORACLE_OCM", "SI_INFORMTN_SCHEMA", "WMSYS", "XS$NULL", "DBSFWUSER",
"REMOTE_SCHEDULER_AGENT", "PDBADMIN", "DGPDB_INT", "OPS$ORACLE",
"GGSYS", "FLOWS_FILES", "APEX_PUBLIC_USER"
);
private static final Pattern OFFSET_FETCH_RE = Pattern.compile(
"(.+?)\\s+OFFSET\\s+(\\d+)\\s+ROWS?\\s+FETCH\\s+(FIRST|NEXT)\\s+(\\d+)\\s+ROWS?\\s+ONLY",
Pattern.CASE_INSENSITIVE
);
private static final Pattern FETCH_ONLY_RE = Pattern.compile(
"(.+?)\\s+FETCH\\s+(FIRST|NEXT)\\s+(\\d+)\\s+ROWS?\\s+ONLY",
Pattern.CASE_INSENSITIVE
);
private static final Pattern PLSQL_OBJECT_DDL_RE = Pattern.compile(
"^\\s*CREATE\\s+(?:OR\\s+REPLACE\\s+)?(?:(?:NON)?EDITIONABLE\\s+)?(?:PROCEDURE|FUNCTION|PACKAGE(?:\\s+BODY)?|TRIGGER|TYPE(?:\\s+BODY)?)\\b",
Pattern.CASE_INSENSITIVE
);
private Connection connection;
@Override
public Connection getConnection() {
return connection;
}
public static String rewriteFetchFirst(String sql) {
Matcher offsetFetch = OFFSET_FETCH_RE.matcher(sql);
if (offsetFetch.matches()) {
String innerSql = offsetFetch.group(1);
long offset = Long.parseLong(offsetFetch.group(2));
long limit = Long.parseLong(offsetFetch.group(4));
long upper = offset + limit;
return "SELECT * FROM (SELECT a.*, ROWNUM rn__ FROM (" + innerSql + ") a WHERE ROWNUM <= " + upper + ") WHERE rn__ > " + offset;
}
Matcher fetchOnly = FETCH_ONLY_RE.matcher(sql);
if (fetchOnly.matches()) {
String innerSql = fetchOnly.group(1);
long limit = Long.parseLong(fetchOnly.group(3));
return "SELECT * FROM (" + innerSql + ") WHERE ROWNUM <= " + limit;
}
return sql;
}
@Override
public String setSchemaSQL(String schema) {
return "ALTER SESSION SET CURRENT_SCHEMA = " + JdbcIdentifiers.INSTANCE.doubleQuote(schema);
}
@Override
public void connect(ConnectParams params) {
uncheckedVoid(() -> {
Class.forName("oracle.jdbc.OracleDriver");
connection = DriverManager.getConnection(buildUrl(params), connectionProperties(params));
});
}
@Override
public boolean testConnection(ConnectParams params) {
return unchecked(() -> {
Class.forName("oracle.jdbc.OracleDriver");
try (Connection conn = DriverManager.getConnection(buildUrl(params), connectionProperties(params))) {
return conn.isValid(5);
}
});
}
@Override
public List<DatabaseInfo> listDatabases() {
return unchecked(() -> {
List<DatabaseInfo> result = new ArrayList<>();
String sql = listDatabasesSql(0);
try (Statement stmt = requireConnected().createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
result.add(new DatabaseInfo(rs.getString(1)));
}
}
return result;
});
}
static String listDatabasesSql(int visibleSchemaCount) {
StringBuilder sql = new StringBuilder()
.append("SELECT owner FROM (")
.append(" SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') AS owner FROM DUAL")
.append(" UNION")
.append(" SELECT DISTINCT owner FROM all_tables")
.append(" UNION")
.append(" SELECT DISTINCT owner FROM all_views")
.append(" )")
.append(" WHERE owner IS NOT NULL")
.append(" AND owner NOT IN (").append(quotedSystemSchemas()).append(")")
.append(" AND owner NOT LIKE 'APEX_%'")
.append(" AND owner NOT LIKE 'FLOWS_%'");
if (visibleSchemaCount > 0) {
sql.append(" AND owner IN (");
for (int i = 0; i < visibleSchemaCount; i++) {
if (i > 0) sql.append(",");
sql.append("?");
}
sql.append(")");
}
sql.append(" ORDER BY owner");
return sql.toString();
}
@Override
public List<String> listSchemas() {
return unchecked(() -> {
List<String> result = new ArrayList<>();
for (DatabaseInfo database : listDatabases()) {
result.add(database.getName());
}
return result;
});
}
@Override
public List<String> listSchemas(List<String> visibleSchemas) {
if (visibleSchemas == null) {
return listSchemas();
}
if (visibleSchemas.isEmpty()) {
return Collections.emptyList();
}
return unchecked(() -> {
List<String> result = new ArrayList<>();
try (PreparedStatement stmt = requireConnected().prepareStatement(listDatabasesSql(visibleSchemas.size()))) {
for (int i = 0; i < visibleSchemas.size(); i++) {
stmt.setString(i + 1, visibleSchemas.get(i));
}
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(rs.getString(1));
}
}
}
return result;
});
}
@Override
public List<TableInfo> listTables(String schema) {
return unchecked(() -> {
List<TableInfo> result = new ArrayList<>();
try (PreparedStatement stmt = requireConnected().prepareStatement(listTablesSql())) {
stmt.setString(1, schema);
stmt.setString(2, schema);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(new TableInfo(rs.getString(1), rs.getString(2), rs.getString(3)));
}
}
}
return result;
});
}
static String listTablesSql() {
return "SELECT OBJECT_NAME, TABLE_TYPE, COMMENTS"
+ " FROM ("
+ " SELECT t.TABLE_NAME AS OBJECT_NAME,"
+ " 'TABLE' AS TABLE_TYPE,"
+ " CAST(NULL AS VARCHAR2(4000)) AS COMMENTS"
+ " FROM ALL_TABLES t"
+ " WHERE t.OWNER = ?"
+ " AND t.NESTED = 'NO'"
+ " UNION ALL"
+ " SELECT o.OBJECT_NAME,"
+ " 'VIEW' AS TABLE_TYPE,"
+ " CAST(NULL AS VARCHAR2(4000)) AS COMMENTS"
+ " FROM ALL_OBJECTS o"
+ " WHERE o.OWNER = ?"
+ " AND o.OBJECT_TYPE = 'VIEW'"
+ " ) ORDER BY OBJECT_NAME";
}
@Override
public List<ObjectInfo> listObjects(String schema) {
return unchecked(() -> {
List<ObjectInfo> result = new ArrayList<>();
try (PreparedStatement stmt = requireConnected().prepareStatement(listObjectsSql())) {
stmt.setString(1, schema);
stmt.setString(2, schema);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(new ObjectInfo(rs.getString(1), rs.getString(2), schema, null));
}
}
}
return result;
});
}
static String listObjectsSql() {
return "SELECT OBJECT_NAME, OBJECT_TYPE"
+ " FROM ("
+ " SELECT t.TABLE_NAME AS OBJECT_NAME,"
+ " 'TABLE' AS OBJECT_TYPE"
+ " FROM ALL_TABLES t"
+ " WHERE t.OWNER = ?"
+ " AND t.NESTED = 'NO'"
+ " UNION ALL"
+ " SELECT o.OBJECT_NAME,"
+ " o.OBJECT_TYPE"
+ " FROM ALL_OBJECTS o"
+ " WHERE o.OWNER = ?"
+ " AND o.OBJECT_TYPE IN ('VIEW', 'PROCEDURE', 'FUNCTION')"
+ " ) ORDER BY CASE OBJECT_TYPE WHEN 'TABLE' THEN 0 WHEN 'VIEW' THEN 1 WHEN 'PROCEDURE' THEN 2 ELSE 3 END, OBJECT_NAME";
}
@Override
public ObjectSource getObjectSource(String schema, String name, String objectType) {
return unchecked(() -> {
String upperType = objectType.toUpperCase(Locale.ROOT);
String source;
if ("VIEW".equals(upperType)) {
// ALL_VIEWS is more reliable than DBMS_METADATA.GET_DDL,
// which fails on XE editions where XSL stylesheets are missing.
String sql = "SELECT TEXT FROM ALL_VIEWS WHERE OWNER = ? AND VIEW_NAME = ?";
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
stmt.setString(1, schema);
stmt.setString(2, name);
try (ResultSet rs = stmt.executeQuery()) {
source = rs.next() ? coalesce(rs.getString(1)) : "";
}
}
} else {
String dbmsType;
switch (upperType) {
case "PROCEDURE":
dbmsType = "PROCEDURE";
break;
case "FUNCTION":
dbmsType = "FUNCTION";
break;
default:
throw new IllegalArgumentException("Unsupported object type: " + objectType);
}
String sql = "SELECT DBMS_METADATA.GET_DDL(?, ?, ?) FROM DUAL";
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
stmt.setString(1, dbmsType);
stmt.setString(2, name);
stmt.setString(3, schema);
try (ResultSet rs = stmt.executeQuery()) {
source = rs.next() ? coalesce(rs.getString(1)) : "";
}
}
}
return new ObjectSource(name, objectType, schema, source);
});
}
@Override
public String getTableDdl(String schema, String table) {
return unchecked(() -> {
String sql = "SELECT DBMS_METADATA.GET_DDL(?, ?, ?) FROM DUAL";
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
stmt.setString(1, "TABLE");
stmt.setString(2, table);
stmt.setString(3, schema);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return coalesce(rs.getString(1));
}
}
}
throw new IllegalArgumentException("Table not found: " + schema + "." + table);
});
}
@Override
public List<ColumnInfo> getColumns(String schema, String table) {
return unchecked(() -> {
List<ColumnInfo> result = new ArrayList<>();
String sql = "SELECT c.COLUMN_NAME, c.DATA_TYPE, c.NULLABLE, c.DATA_PRECISION, c.DATA_SCALE,"
+ " c.DATA_LENGTH, c.CHAR_LENGTH, cc.COMMENTS,"
+ " CASE WHEN pk.COLUMN_NAME IS NULL THEN 0 ELSE 1 END AS IS_PK"
+ " FROM ALL_TAB_COLUMNS c"
+ " LEFT JOIN ALL_COL_COMMENTS cc"
+ " ON cc.OWNER = c.OWNER AND cc.TABLE_NAME = c.TABLE_NAME AND cc.COLUMN_NAME = c.COLUMN_NAME"
+ " LEFT JOIN ("
+ " SELECT cols.COLUMN_NAME FROM ALL_CONS_COLUMNS cols"
+ " JOIN ALL_CONSTRAINTS cons"
+ " ON cols.CONSTRAINT_NAME = cons.CONSTRAINT_NAME AND cols.OWNER = cons.OWNER"
+ " WHERE cons.CONSTRAINT_TYPE = 'P' AND cons.OWNER = ? AND cons.TABLE_NAME = ?"
+ " ) pk ON pk.COLUMN_NAME = c.COLUMN_NAME"
+ " WHERE c.OWNER = ? AND c.TABLE_NAME = ?"
+ " ORDER BY c.COLUMN_ID";
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
stmt.setString(1, schema);
stmt.setString(2, table);
stmt.setString(3, schema);
stmt.setString(4, table);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
String name = rs.getString("COLUMN_NAME");
String baseType = rs.getString("DATA_TYPE");
Integer numPrec = intObject(rs, "DATA_PRECISION");
Integer numScale = intObject(rs, "DATA_SCALE");
Integer dataLen = intObject(rs, "DATA_LENGTH");
Integer charLen = intObject(rs, "CHAR_LENGTH");
String dataType = formatDataType(baseType, numPrec, numScale, dataLen, charLen);
result.add(new ColumnInfo(
name,
dataType,
"Y".equals(rs.getString("NULLABLE")),
null,
rs.getInt("IS_PK") == 1,
null,
rs.getString("COMMENTS"),
numPrec,
numScale,
charLen
));
}
}
}
return result;
});
}
@Override
public List<IndexInfo> listIndexes(String schema, String table) {
return unchecked(() -> {
List<IndexInfo> result = new ArrayList<>();
String sql = "SELECT i.INDEX_NAME,"
+ " RTRIM(XMLAGG(XMLELEMENT(e, ic.COLUMN_NAME || ',') ORDER BY ic.COLUMN_POSITION).EXTRACT('//text()').GETSTRINGVAL(), ',') AS COLUMNS,"
+ " i.UNIQUENESS,"
+ " CASE WHEN c.CONSTRAINT_TYPE = 'P' THEN 1 ELSE 0 END AS IS_PK,"
+ " i.INDEX_TYPE"
+ " FROM ALL_INDEXES i"
+ " JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.OWNER = ic.INDEX_OWNER AND i.TABLE_OWNER = ic.TABLE_OWNER"
+ " LEFT JOIN ALL_CONSTRAINTS c ON i.INDEX_NAME = c.INDEX_NAME AND i.TABLE_OWNER = c.OWNER"
+ " AND c.CONSTRAINT_TYPE = 'P'"
+ " WHERE i.TABLE_OWNER = ? AND i.TABLE_NAME = ?"
+ " GROUP BY i.INDEX_NAME, i.UNIQUENESS, c.CONSTRAINT_TYPE, i.INDEX_TYPE"
+ " ORDER BY i.INDEX_NAME";
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
stmt.setString(1, schema);
stmt.setString(2, table);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(new IndexInfo(
rs.getString(1),
splitNonEmpty(coalesce(rs.getString(2)), ","),
"UNIQUE".equals(rs.getString(3)),
"1".equals(rs.getString(4)),
null,
rs.getString(5),
null,
null
));
}
}
}
return result;
});
}
@Override
public List<ForeignKeyInfo> listForeignKeys(String schema, String table) {
return unchecked(() -> {
List<ForeignKeyInfo> result = new ArrayList<>();
String sql = "SELECT c.CONSTRAINT_NAME, cc.COLUMN_NAME, rc.TABLE_NAME, rcc.COLUMN_NAME"
+ " FROM ALL_CONSTRAINTS c"
+ " JOIN ALL_CONS_COLUMNS cc ON c.CONSTRAINT_NAME = cc.CONSTRAINT_NAME AND c.OWNER = cc.OWNER"
+ " JOIN ALL_CONSTRAINTS rc ON c.R_CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND c.R_OWNER = rc.OWNER"
+ " JOIN ALL_CONS_COLUMNS rcc ON rc.CONSTRAINT_NAME = rcc.CONSTRAINT_NAME AND rc.OWNER = rcc.OWNER"
+ " WHERE c.CONSTRAINT_TYPE = 'R' AND c.OWNER = ? AND c.TABLE_NAME = ?"
+ " ORDER BY c.CONSTRAINT_NAME";
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
stmt.setString(1, schema);
stmt.setString(2, table);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(new ForeignKeyInfo(rs.getString(1), rs.getString(2), rs.getString(3), rs.getString(4)));
}
}
}
return result;
});
}
@Override
public List<TriggerInfo> listTriggers(String schema, String table) {
return unchecked(() -> {
List<TriggerInfo> result = new ArrayList<>();
String sql = "SELECT TRIGGER_NAME, TRIGGERING_EVENT, TRIGGER_TYPE"
+ " FROM ALL_TRIGGERS"
+ " WHERE OWNER = ? AND TABLE_NAME = ?"
+ " ORDER BY TRIGGER_NAME";
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
stmt.setString(1, schema);
stmt.setString(2, table);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(new TriggerInfo(rs.getString(1), rs.getString(2), rs.getString(3)));
}
}
}
return result;
});
}
@Override
public QueryResult executeQuery(String sql, String schema, ExecuteQueryOptions options) {
return JdbcExecutor.INSTANCE.execute(
requireConnected(),
prepareExecutableSql(sql),
schema,
this::setSchemaSQL,
options.getMaxRows(),
options.getFetchSize(),
options.getTimeoutSecs(),
this::stringResultValue
);
}
@Override
public QueryPageResult executeQueryPage(String sql, String schema, QueryPageOptions options) {
return JdbcExecutor.INSTANCE.executePage(
requireConnected(),
prepareExecutableSql(sql),
schema,
this::setSchemaSQL,
options,
this::stringResultValue
);
}
@Override
public QueryPageResult startTableRead(String sql, String schema, QueryPageOptions options) {
return JdbcExecutor.INSTANCE.startTableRead(
requireConnected(),
prepareExecutableSql(sql),
schema,
this::setSchemaSQL,
options,
this::stringResultValue
);
}
@Override
public void disconnect() {
uncheckedVoid(() -> {
if (connection != null) {
connection.close();
}
connection = null;
});
}
private Object stringResultValue(ResultSet rs, int index, int sqlType) {
return unchecked(() -> {
return JdbcExecutor.stringResultValue(rs, index, sqlType);
});
}
static String buildUrl(ConnectParams params) {
String connectionString = params.getConnection_string();
if (connectionString != null && !connectionString.trim().isEmpty()) {
return connectionString;
}
return "jdbc:oracle:thin:@" + params.getHost() + ":" + params.getPort() + "/" + serviceName(params);
}
private static Properties connectionProperties(ConnectParams params) {
Properties props = new Properties();
props.put("user", params.getUsername());
props.put("password", params.getPassword());
if (params.getDatabase().toUpperCase(Locale.ROOT).startsWith("SYSDBA:")) {
props.put("internal_logon", "SYSDBA");
}
return props;
}
private static String serviceName(ConnectParams params) {
String serviceName = params.getDatabase();
if (serviceName.toUpperCase(Locale.ROOT).startsWith("SYSDBA:")) {
return serviceName.substring(7);
}
return serviceName;
}
private static String quotedSystemSchemas() {
List<String> quoted = new ArrayList<>();
for (String schema : SYSTEM_SCHEMAS) {
quoted.add("'" + schema + "'");
}
return String.join(",", quoted);
}
private static String trimTrailingSemicolons(String sql) {
String trimmed = sql;
while (trimmed.endsWith(";")) {
trimmed = trimmed.substring(0, trimmed.length() - 1);
}
return trimmed;
}
static String prepareExecutableSql(String sql) {
String trimmed = sql.trim();
if (PLSQL_OBJECT_DDL_RE.matcher(trimmed).find()) {
return trimmed;
}
return rewriteFetchFirst(trimTrailingSemicolons(trimmed));
}
private static String formatDataType(String base, Integer numPrec, Integer numScale, Integer dataLen, Integer charLen) {
switch (base.toUpperCase(Locale.ROOT)) {
case "VARCHAR2":
case "NVARCHAR2":
case "CHAR":
case "NCHAR":
Integer length = charLen != null ? charLen : dataLen;
return length != null ? base + "(" + length + ")" : base;
case "NUMBER":
if (numPrec != null && numScale != null && numScale > 0) {
return base + "(" + numPrec + "," + numScale + ")";
}
return numPrec != null && numPrec > 0 ? base + "(" + numPrec + ")" : base;
case "RAW":
return dataLen != null ? "RAW(" + dataLen + ")" : "RAW";
default:
return base;
}
}
private static Integer intObject(ResultSet rs, String column) throws Exception {
Object value = rs.getObject(column);
return value == null ? null : ((Number) value).intValue();
}
private static List<String> splitNonEmpty(String value, String delimiter) {
List<String> result = new ArrayList<>();
for (String part : value.split(delimiter)) {
if (!part.isEmpty()) {
result.add(part);
}
}
return result;
}
private static String coalesce(String value) {
return value == null ? "" : value;
}
public static void main(String[] args) {
new JsonRpcServer(new Oracle10gAgent()).run();
}
}

View File

@ -1,235 +0,0 @@
package com.dbx.agent.oracle10g;
import com.dbx.agent.test.TestSupport;
import com.dbx.agent.BaseDatabaseAgent;
import com.dbx.agent.ConnectParams;
import com.dbx.agent.DatabaseAgent;
import com.dbx.agent.ObjectInfo;
import com.dbx.agent.ObjectSource;
import com.dbx.agent.test.JdbcAgentFake;
import com.dbx.agent.test.JdbcFakeExecutionBehaviorTest;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class Oracle10gAgentTest extends JdbcFakeExecutionBehaviorTest {
@Override
protected DatabaseAgent createAgent() {
return new Oracle10gAgent();
}
@Override
protected String resultSetSql() {
return "CALL DBMS_XPLAN.DISPLAY_CURSOR()";
}
@Test
void agentInheritsBaseDatabaseAgent() {
Assertions.assertTrue(createAgent() instanceof BaseDatabaseAgent);
}
@Test
void buildUrlUsesExplicitConnectionString() {
ConnectParams params = new ConnectParams(
"oracle.example.com",
1521,
"ORCL",
"scott",
"tiger",
"",
"jdbc:oracle:thin:@oracle.example.com:1521:ORCL",
false
);
Assertions.assertEquals("jdbc:oracle:thin:@oracle.example.com:1521:ORCL", Oracle10gAgent.buildUrl(params));
}
@Test
void prepareExecutableSqlKeepsPlsqlObjectTerminator() {
String sql = "CREATE OR REPLACE PROCEDURE APP_PROC AS BEGIN NULL; END;";
Assertions.assertEquals(sql, Oracle10gAgent.prepareExecutableSql(sql));
}
@Test
void prepareExecutableSqlTrimsPlainStatementTerminator() {
Assertions.assertEquals("SELECT 1 FROM DUAL", Oracle10gAgent.prepareExecutableSql("SELECT 1 FROM DUAL;"));
}
@Test
void prepareExecutableSqlStillRewritesFetchFirst() {
Assertions.assertEquals(
"SELECT * FROM (SELECT * FROM EMP) WHERE ROWNUM <= 10",
Oracle10gAgent.prepareExecutableSql("SELECT * FROM EMP FETCH FIRST 10 ROWS ONLY;")
);
}
@Test
void listsTablesViewsProceduresAndFunctions() {
Oracle10gAgent agent = new Oracle10gAgent();
TestSupport.setPrivateConnection(agent, objectListConnection());
List<ObjectInfo> objects = agent.listObjects("APP");
Assertions.assertEquals(
Arrays.asList("TABLE", "VIEW", "PROCEDURE", "FUNCTION"),
objects.stream().map(ObjectInfo::getObject_type).collect(Collectors.toList())
);
Assertions.assertEquals(
Arrays.asList("APP_TABLE", "APP_VIEW", "APP_PROC", "APP_FUNC"),
objects.stream().map(ObjectInfo::getName).collect(Collectors.toList())
);
}
@Test
void listTablesSqlUsesSplitDictionaryQuery() {
String sql = Oracle10gAgent.listTablesSql().toUpperCase(Locale.ROOT);
Assertions.assertTrue(sql.contains("ALL_TABLES"), sql);
Assertions.assertTrue(sql.contains("ALL_OBJECTS"), sql);
Assertions.assertTrue(sql.contains("UNION ALL"), sql);
Assertions.assertFalse(sql.contains("ALL_TAB_COMMENTS"), sql);
}
@Test
void listDatabasesSqlCanApplyVisibleSchemaFilter() {
String sql = Oracle10gAgent.listDatabasesSql(2).toUpperCase(Locale.ROOT);
Assertions.assertTrue(sql.contains("ALL_TABLES"), sql);
Assertions.assertTrue(sql.contains("ALL_VIEWS"), sql);
Assertions.assertTrue(sql.contains("OWNER IN (?,?)"), sql);
}
@Test
void listObjectsSqlUsesSplitDictionaryQuery() {
String sql = Oracle10gAgent.listObjectsSql().toUpperCase(Locale.ROOT);
Assertions.assertTrue(sql.contains("ALL_TABLES"), sql);
Assertions.assertTrue(sql.contains("ALL_OBJECTS"), sql);
Assertions.assertTrue(sql.contains("UNION ALL"), sql);
Assertions.assertFalse(sql.contains("ALL_TAB_COMMENTS"), sql);
}
@Test
void loadsRoutineSourceFromDbmsMetadata() {
Oracle10gAgent agent = new Oracle10gAgent();
TestSupport.setPrivateConnection(
agent,
metadataConnection(sql -> Arrays.asList(
Arrays.asList("CREATE OR REPLACE PROCEDURE APP_PROC AS BEGIN NULL; END;")
))
);
ObjectSource source = agent.getObjectSource("APP", "APP_PROC", "PROCEDURE");
Assertions.assertEquals("APP_PROC", source.getName());
Assertions.assertEquals("PROCEDURE", source.getObject_type());
Assertions.assertEquals("APP", source.getSchema());
Assertions.assertEquals("CREATE OR REPLACE PROCEDURE APP_PROC AS BEGIN NULL; END;", source.getSource());
}
private static Connection objectListConnection() {
return metadataConnection(sql -> {
if (sql.contains("'PROCEDURE'")) {
return Arrays.asList(
Arrays.asList("APP_TABLE", "TABLE"),
Arrays.asList("APP_VIEW", "VIEW"),
Arrays.asList("APP_PROC", "PROCEDURE"),
Arrays.asList("APP_FUNC", "FUNCTION")
);
}
return Arrays.asList(
Arrays.asList("APP_TABLE", "TABLE", ""),
Arrays.asList("APP_VIEW", "VIEW", "")
);
});
}
private static Connection metadataConnection(Function<String, List<List<String>>> rowsForSql) {
return proxy(Connection.class, (unused, method, args) -> {
switch (method.getName()) {
case "prepareStatement":
return metadataStatement(rowsForSql.apply((String) args[0]));
case "close":
return null;
case "isClosed":
return false;
default:
return defaultValue(method.getReturnType());
}
});
}
private static PreparedStatement metadataStatement(List<List<String>> rows) {
return proxy(PreparedStatement.class, (unused, method, args) -> {
switch (method.getName()) {
case "executeQuery":
return metadataResultSet(rows);
case "setString":
case "close":
return null;
default:
return defaultValue(method.getReturnType());
}
});
}
private static ResultSet metadataResultSet(List<List<String>> rows) {
int[] index = {-1};
return proxy(ResultSet.class, (unused, method, args) -> {
switch (method.getName()) {
case "next":
index[0] += 1;
return index[0] < rows.size();
case "getString":
return rows.get(index[0]).get(((Integer) args[0]) - 1);
case "close":
return null;
default:
return defaultValue(method.getReturnType());
}
});
}
@SuppressWarnings("unchecked")
private static <T> T proxy(Class<T> type, InvocationHandler handler) {
return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler);
}
private static Object defaultValue(Class<?> type) {
if (type == Boolean.TYPE) {
return false;
}
if (type == Byte.TYPE) {
return (byte) 0;
}
if (type == Short.TYPE) {
return (short) 0;
}
if (type == Integer.TYPE) {
return 0;
}
if (type == Long.TYPE) {
return 0L;
}
if (type == Float.TYPE) {
return 0F;
}
if (type == Double.TYPE) {
return 0.0D;
}
if (type == Character.TYPE) {
return '\0';
}
return null;
}
}

View File

@ -1,19 +0,0 @@
sourceSets {
main {
java {
srcDirs = ['src/main/java', '../oracle/src/main/java']
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.oracle.database.jdbc:ojdbc8:19.26.0.0'
implementation 'com.oracle.database.nls:orai18n:19.26.0.0'
}
tasks.named('shadowJar') {
manifest {
attributes('Agent-Label': 'Oracle Legacy', 'Main-Class': 'com.dbx.agent.oraclelegacy.OracleLegacyAgent')
}
}

View File

@ -1,10 +0,0 @@
package com.dbx.agent.oraclelegacy;
import com.dbx.agent.JsonRpcServer;
import com.dbx.agent.oracle.OracleAgent;
public final class OracleLegacyAgent extends OracleAgent {
public static void main(String[] args) {
new JsonRpcServer(new OracleLegacyAgent()).run();
}
}

View File

@ -1,11 +0,0 @@
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.oracle.database.jdbc:ojdbc17:23.26.2.0.0'
implementation 'com.oracle.database.nls:orai18n:23.26.2.0.0'
}
tasks.named('shadowJar') {
manifest {
attributes('Agent-Label': 'Oracle', 'Main-Class': 'com.dbx.agent.oracle.OracleAgent')
}
}

View File

@ -1,686 +0,0 @@
package com.dbx.agent.oracle;
import com.dbx.agent.BaseDatabaseAgent;
import com.dbx.agent.ColumnInfo;
import com.dbx.agent.ConnectParams;
import com.dbx.agent.DatabaseInfo;
import com.dbx.agent.ExecuteQueryOptions;
import com.dbx.agent.ForeignKeyInfo;
import com.dbx.agent.IndexInfo;
import com.dbx.agent.JdbcExecutor;
import com.dbx.agent.JdbcIdentifiers;
import com.dbx.agent.JsonRpcServer;
import com.dbx.agent.ObjectInfo;
import com.dbx.agent.ObjectSource;
import com.dbx.agent.QueryPageOptions;
import com.dbx.agent.QueryPageResult;
import com.dbx.agent.QueryResult;
import com.dbx.agent.TableInfo;
import com.dbx.agent.TriggerInfo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class OracleAgent extends BaseDatabaseAgent {
private static final Set<String> SYSTEM_SCHEMAS = Set.of(
"SYS", "SYSTEM", "SYSMAN", "DBSNMP", "SYSBACKUP", "SYSDG", "SYSKM", "OUTLN",
"AUDSYS", "LBACSYS", "DVF", "DVSYS", "APPQOSSYS", "CTXSYS", "MDSYS", "MDDATA",
"ORDSYS", "ORDDATA", "ORDPLUGINS", "XDB", "ANONYMOUS", "DIP", "EXFSYS",
"GSMADMIN_INTERNAL", "GSMCATUSER", "GSMUSER", "OJVMSYS", "OLAPSYS",
"ORACLE_OCM", "SI_INFORMTN_SCHEMA", "WMSYS", "XS$NULL", "DBSFWUSER",
"REMOTE_SCHEDULER_AGENT", "PDBADMIN", "DGPDB_INT", "OPS$ORACLE",
"GGSYS", "FLOWS_FILES", "APEX_PUBLIC_USER", "GSMROOTUSER", "SYSRAC"
);
private static final Pattern OFFSET_FETCH_RE = Pattern.compile(
"(.+?)\\s+OFFSET\\s+(\\d+)\\s+ROWS?\\s+FETCH\\s+(FIRST|NEXT)\\s+(\\d+)\\s+ROWS?\\s+ONLY",
Pattern.CASE_INSENSITIVE
);
private static final Pattern FETCH_ONLY_RE = Pattern.compile(
"(.+?)\\s+FETCH\\s+(FIRST|NEXT)\\s+(\\d+)\\s+ROWS?\\s+ONLY",
Pattern.CASE_INSENSITIVE
);
private static final Pattern PLSQL_OBJECT_DDL_RE = Pattern.compile(
"^\\s*CREATE\\s+(?:OR\\s+REPLACE\\s+)?(?:(?:NON)?EDITIONABLE\\s+)?(?:PROCEDURE|FUNCTION|PACKAGE(?:\\s+BODY)?|TRIGGER|TYPE(?:\\s+BODY)?)\\b",
Pattern.CASE_INSENSITIVE
);
private Connection connection;
@Override
public Connection getConnection() {
return connection;
}
public static String rewriteFetchFirst(String sql) {
Matcher offsetMatcher = OFFSET_FETCH_RE.matcher(sql);
if (offsetMatcher.matches()) {
String innerSql = offsetMatcher.group(1);
long offset = Long.parseLong(offsetMatcher.group(2));
long limit = Long.parseLong(offsetMatcher.group(4));
long upper = offset + limit;
return "SELECT * FROM (SELECT a.*, ROWNUM rn__ FROM (" + innerSql + ") a WHERE ROWNUM <= " + upper
+ ") WHERE rn__ > " + offset;
}
Matcher fetchMatcher = FETCH_ONLY_RE.matcher(sql);
if (fetchMatcher.matches()) {
String innerSql = fetchMatcher.group(1);
long limit = Long.parseLong(fetchMatcher.group(3));
return "SELECT * FROM (" + innerSql + ") WHERE ROWNUM <= " + limit;
}
return sql;
}
@Override
public String setSchemaSQL(String schema) {
return "ALTER SESSION SET CURRENT_SCHEMA = " + JdbcIdentifiers.INSTANCE.doubleQuote(schema);
}
@Override
public void connect(ConnectParams params) {
uncheckedVoid(() -> {
Class.forName("oracle.jdbc.OracleDriver");
String serviceName = params.getDatabase();
Properties props = connectionProperties(params);
if (serviceName.toUpperCase(Locale.ROOT).startsWith("SYSDBA:")) {
serviceName = serviceName.substring(7);
props.setProperty("internal_logon", "SYSDBA");
}
connection = DriverManager.getConnection(buildUrl(params, serviceName), props);
try (var stmt = connection.createStatement()) {
stmt.execute("ALTER SESSION SET NLS_LANGUAGE='AMERICAN'");
}
});
}
@Override
public boolean testConnection(ConnectParams params) {
return unchecked(() -> {
Class.forName("oracle.jdbc.OracleDriver");
String serviceName = params.getDatabase();
Properties props = connectionProperties(params);
if (serviceName.toUpperCase(Locale.ROOT).startsWith("SYSDBA:")) {
serviceName = serviceName.substring(7);
props.setProperty("internal_logon", "SYSDBA");
}
try (Connection conn = DriverManager.getConnection(buildUrl(params, serviceName), props)) {
return conn.isValid(5);
}
});
}
@Override
public List<DatabaseInfo> listDatabases() {
return unchecked(() -> {
try {
return queryDatabaseInfos(listDatabasesSql());
} catch (SQLException e) {
if (isPgaLimitError(e)) {
return currentSchemaDatabase();
}
throw e;
}
});
}
static String listDatabasesSql() {
return listDatabasesSql(0);
}
static String listDatabasesSql(int visibleSchemaCount) {
String placeholders = SYSTEM_SCHEMAS.stream()
.map(schema -> "'" + schema + "'")
.collect(Collectors.joining(","));
String visibleSchemaClause = visibleSchemaCount <= 0 ? "" : " AND username IN (" + "?,".repeat(visibleSchemaCount).replaceAll(",$", "") + ")\n";
return """
SELECT username AS owner
FROM all_users
WHERE username IS NOT NULL
AND username NOT IN (%s)
AND username NOT LIKE 'APEX_%%'
AND username NOT LIKE 'FLOWS_%%'
AND username NOT LIKE '%%$%%'
%s
ORDER BY CASE
WHEN username = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') THEN 0
WHEN username = SYS_CONTEXT('USERENV', 'SESSION_USER') THEN 1
ELSE 2
END, username
""".formatted(placeholders, visibleSchemaClause).stripIndent().trim();
}
static boolean isPgaLimitError(SQLException error) {
for (Throwable current = error; current != null; current = current.getCause()) {
if (current instanceof SQLException sqlException) {
if (sqlException.getErrorCode() == 4036 || String.valueOf(sqlException.getMessage()).contains("ORA-04036")) {
return true;
}
SQLException next = sqlException.getNextException();
while (next != null) {
if (next.getErrorCode() == 4036 || String.valueOf(next.getMessage()).contains("ORA-04036")) {
return true;
}
next = next.getNextException();
}
} else if (String.valueOf(current.getMessage()).contains("ORA-04036")) {
return true;
}
}
return false;
}
private List<DatabaseInfo> queryDatabaseInfos(String sql) throws SQLException {
List<DatabaseInfo> result = new ArrayList<>();
try (var stmt = requireConnected().createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
result.add(new DatabaseInfo(rs.getString(1)));
}
}
return result;
}
private List<DatabaseInfo> currentSchemaDatabase() throws SQLException {
try (var stmt = requireConnected().createStatement();
ResultSet rs = stmt.executeQuery("SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL")) {
if (rs.next()) {
String schema = rs.getString(1);
if (schema != null && !schema.isBlank()) {
return List.of(new DatabaseInfo(schema));
}
}
}
return List.of();
}
@Override
public List<String> listSchemas() {
List<String> result = new ArrayList<>();
for (DatabaseInfo database : listDatabases()) {
result.add(database.getName());
}
return result;
}
@Override
public List<String> listSchemas(List<String> visibleSchemas) {
if (visibleSchemas == null) {
return listSchemas();
}
if (visibleSchemas.isEmpty()) {
return List.of();
}
return unchecked(() -> {
List<String> result = new ArrayList<>();
try (var stmt = requireConnected().prepareStatement(listDatabasesSql(visibleSchemas.size()))) {
for (int i = 0; i < visibleSchemas.size(); i++) {
stmt.setString(i + 1, visibleSchemas.get(i));
}
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(rs.getString(1));
}
}
}
return result;
});
}
@Override
public List<TableInfo> listTables(String schema) {
return unchecked(() -> {
try {
List<TableInfo> result = new ArrayList<>();
try (var stmt = requireConnected().prepareStatement(listTablesSql())) {
stmt.setString(1, schema);
stmt.setString(2, schema);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(new TableInfo(rs.getString(1), rs.getString(2), rs.getString(3)));
}
}
}
return result;
} catch (SQLException e) {
if (isPgaLimitError(e)) {
return List.of();
}
throw e;
}
});
}
static String listTablesSql() {
return """
SELECT OBJECT_NAME, TABLE_TYPE, COMMENTS
FROM (
SELECT t.TABLE_NAME AS OBJECT_NAME,
'TABLE' AS TABLE_TYPE,
CAST(NULL AS VARCHAR2(4000)) AS COMMENTS
FROM ALL_TABLES t
WHERE t.OWNER = ?
AND t.NESTED = 'NO'
UNION ALL
SELECT o.OBJECT_NAME,
'VIEW' AS TABLE_TYPE,
CAST(NULL AS VARCHAR2(4000)) AS COMMENTS
FROM ALL_OBJECTS o
WHERE o.OWNER = ?
AND o.OBJECT_TYPE = 'VIEW'
)
ORDER BY OBJECT_NAME
""".stripIndent().trim();
}
@Override
public List<ObjectInfo> listObjects(String schema) {
return unchecked(() -> {
try {
List<ObjectInfo> result = new ArrayList<>();
try (var stmt = requireConnected().prepareStatement(listObjectsSql())) {
stmt.setString(1, schema);
stmt.setString(2, schema);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(new ObjectInfo(rs.getString(1), rs.getString(2), schema, null));
}
}
}
return result;
} catch (SQLException e) {
if (isPgaLimitError(e)) {
return List.of();
}
throw e;
}
});
}
static String listObjectsSql() {
return """
SELECT OBJECT_NAME, OBJECT_TYPE
FROM (
SELECT t.TABLE_NAME AS OBJECT_NAME,
'TABLE' AS OBJECT_TYPE
FROM ALL_TABLES t
WHERE t.OWNER = ?
AND t.NESTED = 'NO'
UNION ALL
SELECT o.OBJECT_NAME,
o.OBJECT_TYPE
FROM ALL_OBJECTS o
WHERE o.OWNER = ?
AND o.OBJECT_TYPE IN ('VIEW', 'PROCEDURE', 'FUNCTION')
)
ORDER BY CASE OBJECT_TYPE WHEN 'TABLE' THEN 0 WHEN 'VIEW' THEN 1 WHEN 'PROCEDURE' THEN 2 ELSE 3 END, OBJECT_NAME
""".stripIndent().trim();
}
@Override
public ObjectSource getObjectSource(String schema, String name, String objectType) {
return unchecked(() -> {
String upperType = objectType.toUpperCase(Locale.ROOT);
String source = "";
if ("VIEW".equals(upperType)) {
// ALL_VIEWS is more reliable than DBMS_METADATA.GET_DDL,
// which fails on XE editions where XSL stylesheets are missing.
try (var stmt = requireConnected().prepareStatement(
"SELECT TEXT FROM ALL_VIEWS WHERE OWNER = ? AND VIEW_NAME = ?")) {
stmt.setString(1, schema);
stmt.setString(2, name);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
String value = rs.getString(1);
source = value == null ? "" : value;
}
}
}
} else {
String dbmsType = switch (upperType) {
case "PROCEDURE" -> "PROCEDURE";
case "FUNCTION" -> "FUNCTION";
default -> throw new IllegalArgumentException("Unsupported object type: " + objectType);
};
try (var stmt = requireConnected().prepareStatement(
"SELECT DBMS_METADATA.GET_DDL(?, ?, ?) FROM DUAL")) {
stmt.setString(1, dbmsType);
stmt.setString(2, name);
stmt.setString(3, schema);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
String value = rs.getString(1);
source = value == null ? "" : value;
}
}
}
}
return new ObjectSource(name, objectType, schema, source);
});
}
@Override
public String getTableDdl(String schema, String table) {
return unchecked(() -> {
try (var stmt = requireConnected().prepareStatement("SELECT DBMS_METADATA.GET_DDL(?, ?, ?) FROM DUAL")) {
stmt.setString(1, "TABLE");
stmt.setString(2, table);
stmt.setString(3, schema);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
String value = rs.getString(1);
return value == null ? "" : value;
}
}
}
throw new IllegalArgumentException("Table not found: " + schema + "." + table);
});
}
@Override
public List<ColumnInfo> getColumns(String schema, String table) {
return unchecked(() -> {
String sql = """
SELECT c.COLUMN_NAME, c.DATA_TYPE, c.NULLABLE, c.DATA_PRECISION, c.DATA_SCALE,
c.DATA_LENGTH, c.CHAR_LENGTH, cc.COMMENTS,
CASE WHEN pk.COLUMN_NAME IS NULL THEN 0 ELSE 1 END AS IS_PK
FROM ALL_TAB_COLUMNS c
LEFT JOIN ALL_COL_COMMENTS cc
ON cc.OWNER = c.OWNER AND cc.TABLE_NAME = c.TABLE_NAME AND cc.COLUMN_NAME = c.COLUMN_NAME
LEFT JOIN (
SELECT cols.COLUMN_NAME FROM ALL_CONS_COLUMNS cols
JOIN ALL_CONSTRAINTS cons
ON cols.CONSTRAINT_NAME = cons.CONSTRAINT_NAME AND cols.OWNER = cons.OWNER
WHERE cons.CONSTRAINT_TYPE = 'P' AND cons.OWNER = ? AND cons.TABLE_NAME = ?
) pk ON pk.COLUMN_NAME = c.COLUMN_NAME
WHERE c.OWNER = ? AND c.TABLE_NAME = ?
ORDER BY c.COLUMN_ID
""".stripIndent().trim();
List<ColumnInfo> result = new ArrayList<>();
try (var stmt = requireConnected().prepareStatement(sql)) {
stmt.setString(1, schema);
stmt.setString(2, table);
stmt.setString(3, schema);
stmt.setString(4, table);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
String name = rs.getString("COLUMN_NAME");
String baseType = rs.getString("DATA_TYPE");
Integer numPrec = intOrNull(rs, "DATA_PRECISION");
Integer numScale = intOrNull(rs, "DATA_SCALE");
Integer dataLen = intOrNull(rs, "DATA_LENGTH");
Integer charLen = intOrNull(rs, "CHAR_LENGTH");
String dataType = formatDataType(baseType, numPrec, numScale, dataLen, charLen);
result.add(new ColumnInfo(
name,
dataType,
"Y".equals(rs.getString("NULLABLE")),
null,
rs.getInt("IS_PK") == 1,
null,
rs.getString("COMMENTS"),
numPrec,
numScale,
charLen
));
}
}
}
return result;
});
}
@Override
public List<IndexInfo> listIndexes(String schema, String table) {
return unchecked(() -> {
String sql = """
SELECT i.INDEX_NAME,
LISTAGG(ic.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY ic.COLUMN_POSITION) AS COLUMNS,
i.UNIQUENESS,
CASE WHEN c.CONSTRAINT_TYPE = 'P' THEN 1 ELSE 0 END AS IS_PK,
i.INDEX_TYPE
FROM ALL_INDEXES i
JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.OWNER = ic.INDEX_OWNER AND i.TABLE_OWNER = ic.TABLE_OWNER
LEFT JOIN ALL_CONSTRAINTS c ON i.INDEX_NAME = c.INDEX_NAME AND i.TABLE_OWNER = c.OWNER
AND c.CONSTRAINT_TYPE = 'P'
WHERE i.TABLE_OWNER = ? AND i.TABLE_NAME = ?
GROUP BY i.INDEX_NAME, i.UNIQUENESS, c.CONSTRAINT_TYPE, i.INDEX_TYPE
ORDER BY i.INDEX_NAME
""".stripIndent().trim();
List<IndexInfo> result = new ArrayList<>();
try (var stmt = requireConnected().prepareStatement(sql)) {
stmt.setString(1, schema);
stmt.setString(2, table);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
String colsStr = rs.getString(2);
List<String> columns = new ArrayList<>();
if (colsStr != null && !colsStr.isEmpty()) {
columns.addAll(List.of(colsStr.split(",")));
}
result.add(new IndexInfo(
rs.getString(1),
columns,
"UNIQUE".equals(rs.getString(3)),
"1".equals(rs.getString(4)),
null,
rs.getString(5),
null,
null
));
}
}
}
return result;
});
}
@Override
public List<ForeignKeyInfo> listForeignKeys(String schema, String table) {
return unchecked(() -> {
String sql = """
SELECT c.CONSTRAINT_NAME, cc.COLUMN_NAME, rc.TABLE_NAME, rcc.COLUMN_NAME
FROM ALL_CONSTRAINTS c
JOIN ALL_CONS_COLUMNS cc ON c.CONSTRAINT_NAME = cc.CONSTRAINT_NAME AND c.OWNER = cc.OWNER
JOIN ALL_CONSTRAINTS rc ON c.R_CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND c.R_OWNER = rc.OWNER
JOIN ALL_CONS_COLUMNS rcc ON rc.CONSTRAINT_NAME = rcc.CONSTRAINT_NAME AND rc.OWNER = rcc.OWNER
WHERE c.CONSTRAINT_TYPE = 'R' AND c.OWNER = ? AND c.TABLE_NAME = ?
ORDER BY c.CONSTRAINT_NAME
""".stripIndent().trim();
List<ForeignKeyInfo> result = new ArrayList<>();
try (var stmt = requireConnected().prepareStatement(sql)) {
stmt.setString(1, schema);
stmt.setString(2, table);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(new ForeignKeyInfo(
rs.getString(1),
rs.getString(2),
rs.getString(3),
rs.getString(4)
));
}
}
}
return result;
});
}
@Override
public List<TriggerInfo> listTriggers(String schema, String table) {
return unchecked(() -> {
String sql = """
SELECT TRIGGER_NAME, TRIGGERING_EVENT, TRIGGER_TYPE
FROM ALL_TRIGGERS
WHERE OWNER = ? AND TABLE_NAME = ?
ORDER BY TRIGGER_NAME
""".stripIndent().trim();
List<TriggerInfo> result = new ArrayList<>();
try (var stmt = requireConnected().prepareStatement(sql)) {
stmt.setString(1, schema);
stmt.setString(2, table);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(new TriggerInfo(rs.getString(1), rs.getString(2), rs.getString(3)));
}
}
}
return result;
});
}
@Override
public QueryResult executeQuery(String sql, String schema, ExecuteQueryOptions options) {
return JdbcExecutor.INSTANCE.execute(
requireConnected(),
prepareExecutableSql(sql),
schema,
this::setSchemaSQL,
options.getMaxRows(),
options.getFetchSize(),
options.getTimeoutSecs(),
OracleAgent::stringResultValue
);
}
@Override
public QueryPageResult executeQueryPage(String sql, String schema, QueryPageOptions options) {
return JdbcExecutor.INSTANCE.executePage(
requireConnected(),
prepareExecutableSql(sql),
schema,
this::setSchemaSQL,
options,
OracleAgent::stringResultValue
);
}
@Override
public QueryPageResult startTableRead(String sql, String schema, QueryPageOptions options) {
return JdbcExecutor.INSTANCE.startTableRead(
requireConnected(),
prepareExecutableSql(sql),
schema,
this::setSchemaSQL,
options,
OracleAgent::stringResultValue
);
}
@Override
public void disconnect() {
uncheckedVoid(() -> {
if (connection != null) {
connection.close();
}
connection = null;
});
}
private static Properties connectionProperties(ConnectParams params) {
Properties props = new Properties();
props.setProperty("user", params.getUsername());
props.setProperty("password", params.getPassword());
props.setProperty("oracle.jdbc.defaultNChar", "true");
return props;
}
static String buildUrl(ConnectParams params) {
return buildUrl(params, serviceName(params));
}
private static String buildUrl(ConnectParams params, String serviceName) {
String connectionString = params.getConnection_string();
if (connectionString != null && !connectionString.trim().isEmpty()) {
return connectionString;
}
return "jdbc:oracle:thin:@" + params.getHost() + ":" + params.getPort() + "/" + serviceName;
}
private static String serviceName(ConnectParams params) {
String serviceName = params.getDatabase();
if (serviceName.toUpperCase(Locale.ROOT).startsWith("SYSDBA:")) {
return serviceName.substring(7);
}
return serviceName;
}
private static String formatDataType(
String base,
Integer numPrec,
Integer numScale,
Integer dataLen,
Integer charLen
) {
return switch (base.toUpperCase(Locale.ROOT)) {
case "VARCHAR2", "NVARCHAR2", "CHAR", "NCHAR" -> {
Integer len = charLen == null ? dataLen : charLen;
yield len == null ? base : base + "(" + len + ")";
}
case "NUMBER" -> {
if (numPrec != null && numScale != null && numScale > 0) {
yield base + "(" + numPrec + "," + numScale + ")";
}
if (numPrec != null && numPrec > 0) {
yield base + "(" + numPrec + ")";
}
yield base;
}
case "RAW" -> dataLen == null ? "RAW" : "RAW(" + dataLen + ")";
default -> base;
};
}
private static Object stringResultValue(ResultSet rs, int index, int sqlType) {
try {
return JdbcExecutor.stringResultValue(rs, index, sqlType);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private static Integer intOrNull(ResultSet rs, String column) throws Exception {
Object value = rs.getObject(column);
return value instanceof Number ? ((Number) value).intValue() : null;
}
private static String trimEndSemicolons(String sql) {
String trimmed = sql;
while (trimmed.endsWith(";")) {
trimmed = trimmed.substring(0, trimmed.length() - 1);
}
return trimmed;
}
static String prepareExecutableSql(String sql) {
String trimmed = sql.trim();
if (PLSQL_OBJECT_DDL_RE.matcher(trimmed).find()) {
return trimmed;
}
return rewriteFetchFirst(trimEndSemicolons(trimmed));
}
public static void main(String[] args) {
new JsonRpcServer(new OracleAgent()).run();
}
}

View File

@ -1,113 +0,0 @@
package com.dbx.agent.oracle;
import com.dbx.agent.DatabaseAgent;
import com.dbx.agent.ConnectParams;
import com.dbx.agent.test.JdbcFakeExecutionBehaviorTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import java.util.List;
import java.util.Locale;
class OracleAgentTest extends JdbcFakeExecutionBehaviorTest {
@Override
protected DatabaseAgent createAgent() {
return new OracleAgent();
}
@Override
protected String resultSetSql() {
return "CALL DBMS_XPLAN.DISPLAY_CURSOR()";
}
@Test
void buildUrlUsesExplicitConnectionString() {
ConnectParams params = new ConnectParams(
"oracle.example.com",
1521,
"ORCL",
"scott",
"tiger",
"",
"jdbc:oracle:thin:@oracle.example.com:1521:ORCL",
false
);
Assertions.assertEquals("jdbc:oracle:thin:@oracle.example.com:1521:ORCL", OracleAgent.buildUrl(params));
}
@Test
void prepareExecutableSqlKeepsPlsqlObjectTerminator() {
String sql = "CREATE OR REPLACE PROCEDURE APP_PROC AS BEGIN NULL; END;";
Assertions.assertEquals(sql, OracleAgent.prepareExecutableSql(sql));
}
@Test
void prepareExecutableSqlTrimsPlainStatementTerminator() {
Assertions.assertEquals("SELECT 1 FROM DUAL", OracleAgent.prepareExecutableSql("SELECT 1 FROM DUAL;"));
}
@Test
void prepareExecutableSqlStillRewritesFetchFirst() {
Assertions.assertEquals(
"SELECT * FROM (SELECT * FROM EMP) WHERE ROWNUM <= 10",
OracleAgent.prepareExecutableSql("SELECT * FROM EMP FETCH FIRST 10 ROWS ONLY;")
);
}
@Test
void listDatabasesSqlUsesUserDictionaryInsteadOfObjectDictionary() {
String sql = OracleAgent.listDatabasesSql().toUpperCase(Locale.ROOT);
Assertions.assertTrue(sql.contains("ALL_USERS"));
Assertions.assertFalse(sql.contains("ALL_TABLES"));
Assertions.assertFalse(sql.contains("ALL_VIEWS"));
}
@Test
void listDatabasesSqlCanApplyVisibleSchemaFilter() {
String sql = OracleAgent.listDatabasesSql(2).toUpperCase(Locale.ROOT);
Assertions.assertTrue(sql.contains("ALL_USERS"), sql);
Assertions.assertTrue(sql.contains("USERNAME IN (?,?)"), sql);
Assertions.assertFalse(sql.contains("ALL_TABLES"), sql);
}
@Test
void listSchemasWithEmptyVisibleFilterSkipsMetadataQuery() {
OracleAgent agent = new OracleAgent();
Assertions.assertEquals(List.of(), agent.listSchemas(List.of()));
}
@Test
void listTablesSqlUsesSplitDictionaryQuery() {
String sql = OracleAgent.listTablesSql().toUpperCase(Locale.ROOT);
Assertions.assertTrue(sql.contains("ALL_TABLES"), sql);
Assertions.assertTrue(sql.contains("ALL_OBJECTS"), sql);
Assertions.assertTrue(sql.contains("UNION ALL"), sql);
Assertions.assertFalse(sql.contains("ALL_TAB_COMMENTS"), sql);
}
@Test
void listObjectsSqlUsesSplitDictionaryQuery() {
String sql = OracleAgent.listObjectsSql().toUpperCase(Locale.ROOT);
Assertions.assertTrue(sql.contains("ALL_TABLES"), sql);
Assertions.assertTrue(sql.contains("ALL_OBJECTS"), sql);
Assertions.assertTrue(sql.contains("UNION ALL"), sql);
Assertions.assertFalse(sql.contains("ALL_TAB_COMMENTS"), sql);
}
@Test
void detectsPgaLimitError() {
Assertions.assertTrue(OracleAgent.isPgaLimitError(new SQLException(
"ORA-04036: PGA memory used by the instance exceeds PGA_AGGREGATE_LIMIT",
"72000",
4036
)));
}
}

View File

@ -11,9 +11,11 @@ SOURCE_GLOBS = ("*/src/main/**/*.java", "drivers/*/src/main/**/*.java")
KOTLIN_FILE_SUFFIXES = (".kt", ".kts")
KOTLIN_SCAN_EXCLUDED_PARTS = {".git", ".gradle", "build"}
DEFAULT_AGENT_JRE_KEY = "21"
LEGACY_ORACLE_JRE_KEY = "8"
NON_JDBC_AGENT_MODULES = {"mongodb", "etcd", "zookeeper"}
NATIVE_ONLY_AGENT_MODULES = {"xugu"}
NATIVE_ONLY_AGENT_MODULES = {
"oracle": "drivers/oracle-go",
"xugu": "drivers/xugu",
}
JDBC_ARCHITECTURE_ALLOWLIST = {
"access": "custom Access metadata and URL behavior pending migration",
"dameng": "custom Dameng metadata and DDL pending migration",
@ -23,9 +25,6 @@ JDBC_ARCHITECTURE_ALLOWLIST = {
"goldendb": "custom GoldenDB metadata pending migration",
"informix": "custom Informix metadata pending migration",
"neo4j": "custom Neo4j transaction/query behavior pending migration",
"oracle": "custom Oracle metadata and connection properties pending migration",
"oracle-legacy": "custom Oracle legacy metadata and connection properties pending migration",
"oracle-10g": "custom Oracle 10g metadata and Java 8 runtime pending migration",
"sundb": "custom SunDB metadata pending migration",
"tdengine": "custom TDengine WebSocket JDBC behavior pending migration",
}
@ -69,11 +68,13 @@ def included_agent_modules(root: Path) -> set[str]:
def agent_modules(root: Path) -> set[str]:
native = {name for name in NATIVE_ONLY_AGENT_MODULES if module_dir(root, name).exists()}
native = {name for name, path in NATIVE_ONLY_AGENT_MODULES.items() if (root / path).exists()}
return included_agent_modules(root) | native
def module_dir(root: Path, module: str) -> Path:
if module in NATIVE_ONLY_AGENT_MODULES:
return root / NATIVE_ONLY_AGENT_MODULES[module]
nested = root / "drivers" / module
if nested.exists():
return nested
@ -162,7 +163,7 @@ def validate_manifest_fields(root: Path, modules: set[str]) -> list[str]:
def validate_jdbc_architecture(root: Path, modules: set[str]) -> list[str]:
problems: list[str] = []
for module in sorted(modules - NON_JDBC_AGENT_MODULES - NATIVE_ONLY_AGENT_MODULES):
for module in sorted(modules - NON_JDBC_AGENT_MODULES - set(NATIVE_ONLY_AGENT_MODULES)):
source = main_class_source(root, module)
if source is None or not source.exists():
continue
@ -227,13 +228,9 @@ def validate_release_runtime_keys(root: Path) -> list[str]:
rf'java-version:\s*"{DEFAULT_AGENT_JRE_KEY}"',
f"release workflow must build the default JRE from Java {DEFAULT_AGENT_JRE_KEY}",
),
(
rf'oracle-10g\)\s*echo\s*"{LEGACY_ORACLE_JRE_KEY}"',
f"oracle-10g must keep JRE key {LEGACY_ORACLE_JRE_KEY}",
),
(
rf'\*\)\s*echo\s*"{DEFAULT_AGENT_JRE_KEY}"',
f"non-legacy agents must use JRE key {DEFAULT_AGENT_JRE_KEY}",
f"agents must use JRE key {DEFAULT_AGENT_JRE_KEY}",
),
(
rf'"{DEFAULT_AGENT_JRE_KEY}":\s*\{{\s*"version":\s*"{DEFAULT_AGENT_JRE_KEY}\.',
@ -249,7 +246,7 @@ def validate_release_runtime_keys(root: Path) -> list[str]:
problems.append(message)
forbidden_patterns = [
(r'jre-key:\s*"17"', "release workflow must not build Java 21 under JRE key 17"),
(r'\*\)\s*echo\s*"17"', "non-legacy agents must not use JRE key 17"),
(r'\*\)\s*echo\s*"17"', "agents must not use JRE key 17"),
(r'"17":\s*\{\s*"version":\s*"21\.', "registry must not publish Java 21 under JRE key 17"),
]
for pattern, message in forbidden_patterns:

View File

@ -127,33 +127,22 @@ class ValidateAgentsTest(unittest.TestCase):
problems,
)
def test_jdbc_architecture_allows_documented_migration_exceptions(self):
def test_versions_include_native_only_modules(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
module = root / "oracle"
source = module / "src/main/java/com/dbx/agent/oracle/OracleAgent.java"
source.parent.mkdir(parents=True)
(module / "build.gradle").write_text(
textwrap.dedent(
"""
tasks.named('shadowJar') {
manifest {
attributes(
'Agent-Label': 'Oracle',
'Main-Class': 'com.dbx.agent.oracle.OracleAgent'
)
}
}
"""
),
(root / "settings.gradle").write_text(
"def infrastructureModules = ['common', 'test-support']\n"
"def driverModules = ['h2']\n"
"include(*(infrastructureModules + driverModules))\n",
encoding="utf-8",
)
source.write_text(
"package com.dbx.agent.oracle; public final class OracleAgent extends BaseDatabaseAgent {}\n",
(root / "drivers/oracle-go").mkdir(parents=True)
(root / "versions.json").write_text(
json.dumps({"h2": "0.1.0", "oracle": "0.1.0"}),
encoding="utf-8",
)
self.assertEqual([], validate_agents.validate_jdbc_architecture(root, {"oracle"}))
self.assertEqual([], validate_agents.validate_versions(root))
def test_authoring_template_must_use_shared_foundation(self):
with tempfile.TemporaryDirectory() as tmp:
@ -316,7 +305,7 @@ class ValidateAgentsTest(unittest.TestCase):
problems,
)
def test_release_runtime_keys_match_java_21_default_and_oracle_8_exception(self):
def test_release_runtime_keys_match_java_21_default(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
workflow = root / ".github/workflows/release.yml"
@ -331,7 +320,6 @@ class ValidateAgentsTest(unittest.TestCase):
java-version: "21"
detect_jre_key() {
case "$name" in
oracle-10g) echo "8" ;;
*) echo "21" ;;
esac
}
@ -362,7 +350,6 @@ class ValidateAgentsTest(unittest.TestCase):
java-version: "21"
detect_jre_key() {
case "$name" in
oracle-10g) echo "8" ;;
*) echo "17" ;;
esac
}
@ -383,11 +370,11 @@ class ValidateAgentsTest(unittest.TestCase):
self.assertEqual(
[
"release workflow must build the default JRE with key 21",
"non-legacy agents must use JRE key 21",
"agents must use JRE key 21",
"registry must publish Java 21 under JRE key 21",
"native-only registry entries must publish a legacy jar placeholder for older DBX clients",
"release workflow must not build Java 21 under JRE key 17",
"non-legacy agents must not use JRE key 17",
"agents must not use JRE key 17",
"registry must not publish Java 21 under JRE key 17",
],
validate_agents.validate_release_runtime_keys(root),

View File

@ -3,8 +3,8 @@ rootProject.name = 'dbx-agents'
def infrastructureModules = ['common', 'test-support']
def driverModules = [
'access', 'dameng', 'kingbase', 'vastbase', 'goldendb', 'databend', 'databricks', 'saphana',
'teradata', 'vertica', 'firebird', 'exasol', 'oceanbase-oracle', 'gbase8a', 'gbase8s', 'oracle',
'oracle-legacy', 'oracle-10g', 'bigquery', 'kylin', 'sundb', 'h2', 'snowflake', 'trino', 'hive',
'teradata', 'vertica', 'firebird', 'exasol', 'oceanbase-oracle', 'gbase8a', 'gbase8s',
'bigquery', 'kylin', 'sundb', 'h2', 'snowflake', 'trino', 'hive',
'db2', 'informix', 'neo4j', 'cassandra', 'gaussdb', 'mongodb', 'highgo', 'tdengine', 'yashandb',
'iris', 'iotdb', 'etcd', 'zookeeper'
]

View File

@ -20,8 +20,6 @@
"neo4j": "0.1.25",
"oceanbase-oracle": "0.1.20",
"oracle": "0.1.29",
"oracle-legacy": "0.1.16",
"oracle-10g": "0.1.28",
"snowflake": "0.1.25",
"saphana": "0.1.20",
"sundb": "0.1.25",

View File

@ -251,9 +251,6 @@ pub fn oracle_alternate_connect_configs(config: &ConnectionConfig, err: &str) ->
if config.db_type != DatabaseType::Oracle {
return Vec::new();
}
if config.driver_profile.as_deref() == Some("oracle-10g") {
return Vec::new();
}
if config.connection_string.as_deref().is_some_and(|value| !value.trim().is_empty()) {
return Vec::new();
}
@ -402,21 +399,6 @@ fn postgres_like_agent_jdbc_connection_string(
append_agent_url_params(base, config.url_params.as_deref())
}
pub fn should_retry_oracle_with_10g_driver(config: &ConnectionConfig, err: &str) -> bool {
!oracle_auth_fallback_profiles(config, err).is_empty()
}
pub fn oracle_auth_fallback_profiles(config: &ConnectionConfig, err: &str) -> Vec<&'static str> {
if config.db_type != DatabaseType::Oracle {
return Vec::new();
}
let normalized = err.to_lowercase();
if !normalized.contains("ora-28040") && !normalized.contains("no matching authentication protocol") {
return Vec::new();
}
Vec::new()
}
pub fn oracle_alternate_connect_config(config: &ConnectionConfig, err: &str) -> Option<ConnectionConfig> {
oracle_alternate_connect_configs(config, err).into_iter().next()
}
@ -749,10 +731,9 @@ mod tests {
}
#[test]
fn oracle_listener_error_hint_skips_other_databases() {
fn oracle_listener_error_hint_skips_non_oracle_databases() {
let err = "Agent RPC error (-1): ORA-12541: TNS:no listener";
let mut cfg = config(DatabaseType::Oracle, Some("ORCL"));
cfg.driver_profile = Some("oracle-legacy".to_string());
assert!(oracle_error_with_driver_hint(&cfg, err).contains("Service Name"));
@ -909,20 +890,6 @@ mod tests {
assert!(oracle_alternate_connect_config(&cfg, "ORA-12541: TNS:no listener").is_some());
}
#[test]
fn oracle_auth_errors_do_not_switch_driver_profiles() {
let mut cfg = config(DatabaseType::Oracle, Some("ORCL"));
cfg.driver_profile = Some("oracle".to_string());
assert!(oracle_auth_fallback_profiles(&cfg, "ORA-28040: No matching authentication protocol").is_empty());
cfg.driver_profile = Some("oracle-legacy".to_string());
assert!(oracle_auth_fallback_profiles(&cfg, "ORA-28040: No matching authentication protocol").is_empty());
cfg.driver_profile = Some("oracle-10g".to_string());
assert!(oracle_auth_fallback_profiles(&cfg, "ORA-28040: No matching authentication protocol").is_empty());
}
#[test]
fn oracle_custom_connection_string_skips_alternate_descriptor_retry() {
let mut cfg = config(DatabaseType::Oracle, Some("ORCL"));

View File

@ -215,8 +215,7 @@ mod tests {
assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Oracle, None), Some("oracle"));
assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Gbase, Some("gbase8s")), Some("gbase8s"));
assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Gbase, None), Some("gbase8a"));
manager.stop_daemon_by_key("oracle-legacy").await;
manager.stop_daemon_by_key("oracle-10g").await;
manager.stop_daemon_by_key("oracle").await;
manager.stop_daemon_by_key("gbase8s").await;
}
@ -486,7 +485,7 @@ impl AgentManager {
if flat.exists() {
return flat;
}
// macOS Adoptium JRE 8 uses Contents/Home/ layout
// Some macOS runtimes are unpacked with a Contents/Home/ layout.
let macos = dir.join("Contents").join("Home").join("bin").join(java_name);
if macos.exists() {
return macos;

View File

@ -127,13 +127,6 @@ pub async fn call_daemon_method_with_timeout<T: DeserializeOwned + Send + 'stati
fn runtime_agent_key_candidates(db_type: &DatabaseType, driver_profile: Option<&str>) -> Option<Vec<&'static str>> {
let primary = db_type_to_agent_key(db_type, driver_profile)?;
if *db_type == DatabaseType::Oracle {
return match driver_profile {
Some("oracle-legacy") => Some(vec![primary, "oracle-legacy"]),
Some("oracle-10g") => Some(vec![primary, "oracle-10g"]),
_ => Some(vec![primary]),
};
}
Some(vec![primary])
}

View File

@ -11,8 +11,7 @@ use mysql_async::Row as MysqlRow;
use crate::agent_connection::{
agent_connect_params, h2_file_path_from_jdbc_url, is_h2_file_connection, mongo_legacy_error_with_auth_hint,
mongo_uses_legacy_driver, oracle_alternate_connect_config_labels, oracle_alternate_connect_configs,
oracle_auth_fallback_profiles, oracle_error_with_driver_hint, should_retry_mongo_with_legacy_driver,
should_retry_oracle_with_10g_driver, trino_like_jdbc_connection_string,
oracle_error_with_driver_hint, should_retry_mongo_with_legacy_driver, trino_like_jdbc_connection_string,
};
use crate::agent_manager::{JavaRuntimeMode, DEFAULT_JRE_KEY};
use crate::database_capabilities;
@ -899,7 +898,7 @@ impl AppState {
let connect_result = client
.call_method_with_timeout::<serde_json::Value>(
AgentMethod::Connect,
connect_params.clone(),
connect_params,
Some(agent_connect_timeout(&db_config)),
)
.await;
@ -947,45 +946,6 @@ impl AppState {
fallback_errors.join("\n")
));
}
} else if should_retry_oracle_with_10g_driver(&db_config, &err) {
log::warn!(
"Oracle connect failed with profile {:?}: {}. Retrying with legacy Oracle profiles.",
db_config.driver_profile,
err
);
let mut fallback_errors = Vec::new();
let mut connected_client = None;
for profile in oracle_auth_fallback_profiles(&db_config, &err) {
match self.agent_manager.spawn(&db_config.db_type, Some(profile)).await {
Ok(mut fallback_client) => {
match fallback_client
.call_method_with_timeout::<serde_json::Value>(
AgentMethod::Connect,
connect_params.clone(),
Some(agent_connect_timeout(&db_config)),
)
.await
{
Ok(_) => {
connected_client = Some(fallback_client);
break;
}
Err(fallback_err) => {
fallback_errors.push(format!("{profile}: {fallback_err}"));
}
}
}
Err(fallback_err) => {
fallback_errors.push(format!("{profile}: {fallback_err}"));
}
}
}
client = connected_client.ok_or_else(|| {
format!(
"{err}\n\nFallback with legacy Oracle drivers failed: {}",
fallback_errors.join("\n")
)
})?;
} else {
return Err(oracle_error_with_driver_hint(&db_config, &err));
}
@ -2398,7 +2358,7 @@ mod tests {
};
use crate::agent_connection::{
agent_connect_params, mongo_legacy_error_with_auth_hint, mongo_uses_legacy_driver,
oracle_alternate_connect_config, should_retry_mongo_with_legacy_driver, should_retry_oracle_with_10g_driver,
oracle_alternate_connect_config, should_retry_mongo_with_legacy_driver,
};
use crate::agent_manager::{AgentState, JavaRuntimeConfig, JavaRuntimeMode, DEFAULT_JRE_KEY};
use crate::database_capabilities;
@ -2692,33 +2652,6 @@ mod tests {
assert_eq!(params["connection_string"], "jdbc:oracle:thin:@//127.0.0.1:11521/ORCL");
}
#[test]
fn oracle_retry_guard_only_triggers_for_non_10g_listener_errors() {
let mut config = mysql_config(Some("ORCL"));
config.db_type = DatabaseType::Oracle;
config.driver_profile = Some("oracle".to_string());
assert!(!should_retry_oracle_with_10g_driver(
&config,
"Agent RPC error (-1): ORA-28040: No matching authentication protocol"
));
assert!(!should_retry_oracle_with_10g_driver(&config, "Agent RPC error (-1): ORA-12541: TNS:no listener"));
assert!(!should_retry_oracle_with_10g_driver(&config, "host xxx port 1521 中没有监听程序"));
config.driver_profile = Some("oracle-10g".to_string());
assert!(!should_retry_oracle_with_10g_driver(&config, "Agent RPC error (-1): ORA-12541: TNS:no listener"));
assert!(!should_retry_oracle_with_10g_driver(
&config,
"Agent RPC error (-1): ORA-28040: No matching authentication protocol"
));
config.driver_profile = Some("oracle".to_string());
assert!(!should_retry_oracle_with_10g_driver(
&config,
"Agent RPC error (-1): ORA-01017: invalid username/password"
));
}
#[test]
fn oracle_listener_errors_can_retry_with_alternate_connect_descriptor() {
let mut config = mysql_config(Some("ORCL"));
@ -2747,15 +2680,12 @@ mod tests {
}
#[test]
fn oracle_alternate_descriptor_retry_skips_non_listener_errors_and_10g_profiles() {
fn oracle_alternate_descriptor_retry_skips_non_listener_errors() {
let mut config = mysql_config(Some("ORCL"));
config.db_type = DatabaseType::Oracle;
config.driver_profile = Some("oracle".to_string());
assert!(oracle_alternate_connect_config(&config, "ORA-01017: invalid username/password").is_none());
config.driver_profile = Some("oracle-10g".to_string());
assert!(oracle_alternate_connect_config(&config, "ORA-12514: listener does not know service").is_none());
}
#[test]

View File

@ -1104,9 +1104,7 @@ fn agent_java_args(jar_path: &str) -> Vec<String> {
args.push("-Djava.net.preferIPv4Stack=true".to_string());
}
if !agent_jar_path_matches_key(jar_path, "oracle-10g") {
args.push("--add-opens=java.sql/java.sql=ALL-UNNAMED".to_string());
}
args.push("--add-opens=java.sql/java.sql=ALL-UNNAMED".to_string());
args.extend(["-XX:TieredStopAtLevel=1", "-XX:+UseSerialGC", "-jar", jar_path].into_iter().map(str::to_string));
@ -1286,13 +1284,6 @@ mod tests {
assert!(args.iter().any(|arg| arg == "--add-opens=java.sql/java.sql=ALL-UNNAMED"));
}
#[test]
fn agent_java_args_skip_module_flags_for_oracle_10g_profile() {
let args = agent_java_args("/tmp/dbx/drivers/oracle-10g/agent.jar");
assert!(!args.iter().any(|arg| arg == "--add-opens=java.sql/java.sql=ALL-UNNAMED"));
}
#[test]
fn agent_java_args_disable_ambient_proxy_settings() {
let args = agent_java_args("/tmp/dbx-agent-opengauss.jar");

View File

@ -26,11 +26,7 @@ test("uses the unified Oracle driver for legacy Oracle profiles", () => {
assert.equal(
showAgentDriverInstallHint(
"oracle",
[
{ db_type: "oracle", installed: false },
{ db_type: "oracle-legacy", installed: false },
{ db_type: "oracle-10g", installed: true },
],
[{ db_type: "oracle", installed: false }],
"oracle-10g",
),
true,
@ -38,11 +34,7 @@ test("uses the unified Oracle driver for legacy Oracle profiles", () => {
assert.equal(
showAgentDriverInstallHint(
"oracle",
[
{ db_type: "oracle", installed: true },
{ db_type: "oracle-legacy", installed: false },
{ db_type: "oracle-10g", installed: false },
],
[{ db_type: "oracle", installed: true }],
"oracle",
),
false,
@ -50,11 +42,7 @@ test("uses the unified Oracle driver for legacy Oracle profiles", () => {
assert.equal(
showAgentDriverInstallHint(
"oracle",
[
{ db_type: "oracle", installed: true },
{ db_type: "oracle-legacy", installed: true },
{ db_type: "oracle-10g", installed: false },
],
[{ db_type: "oracle", installed: true }],
"oracle-legacy",
),
false,

View File

@ -4,8 +4,7 @@ use tauri::State;
pub use dbx_core::agent_connection::{
agent_connect_params, mongo_legacy_error_with_auth_hint, mongo_uses_legacy_driver, oracle_alternate_connect_config,
oracle_auth_fallback_profiles, oracle_error_with_driver_hint, should_retry_mongo_with_legacy_driver,
should_retry_oracle_with_10g_driver,
oracle_error_with_driver_hint, should_retry_mongo_with_legacy_driver,
};
pub use dbx_core::connection::{
agent_connect_timeout, connect_bare_metadata_pool, connect_mysql_metadata_pool, connection_url_for_endpoint,
@ -67,7 +66,7 @@ async fn test_agent_connection(
&config.db_type,
config.driver_profile.as_deref(),
AgentMethod::TestConnection,
connect_params.clone(),
connect_params,
Some(agent_connect_timeout(config)),
)
.await;
@ -92,34 +91,6 @@ async fn test_agent_connection(
.map_err(|alternate_err| {
format!("{err}\n\nFallback with alternate Oracle descriptor failed: {alternate_err}")
})?;
} else if should_retry_oracle_with_10g_driver(config, &err) {
let mut fallback_errors = Vec::new();
let mut connected = false;
for profile in oracle_auth_fallback_profiles(config, &err) {
match state
.agent_manager
.call_daemon_method_with_timeout::<serde_json::Value>(
&config.db_type,
Some(profile),
AgentMethod::TestConnection,
connect_params.clone(),
Some(agent_connect_timeout(config)),
)
.await
{
Ok(_) => {
connected = true;
break;
}
Err(fallback_err) => fallback_errors.push(format!("{profile}: {fallback_err}")),
}
}
if !connected {
return Err(format!(
"{err}\n\nFallback with legacy Oracle drivers failed: {}",
fallback_errors.join("\n")
));
}
} else {
return Err(oracle_error_with_driver_hint(config, &err));
}
@ -139,7 +110,7 @@ async fn connect_agent_pool(
let connect_result = client
.call_method_with_timeout::<serde_json::Value>(
AgentMethod::Connect,
connect_params.clone(),
connect_params,
Some(agent_connect_timeout(config)),
)
.await;
@ -161,33 +132,6 @@ async fn connect_agent_pool(
.map_err(|alternate_err| {
format!("{err}\n\nFallback with alternate Oracle descriptor failed: {alternate_err}")
})?;
} else if should_retry_oracle_with_10g_driver(config, &err) {
let mut fallback_errors = Vec::new();
let mut connected_client = None;
for profile in oracle_auth_fallback_profiles(config, &err) {
match state.agent_manager.spawn(&config.db_type, Some(profile)).await {
Ok(mut fallback_client) => {
match fallback_client
.call_method_with_timeout::<serde_json::Value>(
AgentMethod::Connect,
connect_params.clone(),
Some(agent_connect_timeout(config)),
)
.await
{
Ok(_) => {
connected_client = Some(fallback_client);
break;
}
Err(fallback_err) => fallback_errors.push(format!("{profile}: {fallback_err}")),
}
}
Err(fallback_err) => fallback_errors.push(format!("{profile}: {fallback_err}")),
}
}
client = connected_client.ok_or_else(|| {
format!("{err}\n\nFallback with legacy Oracle drivers failed: {}", fallback_errors.join("\n"))
})?;
} else {
return Err(oracle_error_with_driver_hint(config, &err));
}