feat(agents): move agents into dbx repo
This commit is contained in:
1 parent
ab35fda299
commit
3a25487dea
215 files changed
+29740
-15
No files matched your search
@@ -0,0 +1,159 @@
|
||||
package com.dbx.agent.iris;
|
||||
|
||||
import com.dbx.agent.ConfiguredJdbcAgent;
|
||||
import com.dbx.agent.ColumnInfo;
|
||||
import com.dbx.agent.JdbcAgentProfile;
|
||||
import com.dbx.agent.JsonRpcServer;
|
||||
import com.dbx.agent.StandardJdbcMetadata;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class IrisAgent extends ConfiguredJdbcAgent {
|
||||
public static final JdbcAgentProfile IRIS_PROFILE = new JdbcAgentProfile(
|
||||
"com.intersystems.jdbc.IRISDriver",
|
||||
"jdbc:IRIS://{host}:{port}/{database}",
|
||||
1972,
|
||||
true
|
||||
);
|
||||
|
||||
public IrisAgent() {
|
||||
super(IRIS_PROFILE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> listSchemas() {
|
||||
return dedupeCaseInsensitiveSchemas(StandardJdbcMetadata.INSTANCE.listSchemas(requireConnection(), IRIS_PROFILE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ColumnInfo> getColumns(String schema, String table) {
|
||||
return irisColumns(requireConnection(), schema, table);
|
||||
}
|
||||
|
||||
static List<ColumnInfo> irisColumns(Connection conn, String schema, String table) {
|
||||
return unchecked(() -> {
|
||||
Set<String> primaryKeys = irisPrimaryKeys(conn, schema, table);
|
||||
List<ColumnInfo> columns = new ArrayList<>();
|
||||
try (PreparedStatement stmt = conn.prepareStatement(columnSql(schema))) {
|
||||
int index = 1;
|
||||
if (!isBlank(schema)) {
|
||||
stmt.setString(index++, schema.trim());
|
||||
}
|
||||
stmt.setString(index, table);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
String name = rs.getString("COLUMN_NAME");
|
||||
if (isBlank(name)) {
|
||||
continue;
|
||||
}
|
||||
columns.add(new ColumnInfo(
|
||||
name,
|
||||
stringOrDefault(rs, "DATA_TYPE", "Unknown"),
|
||||
!"NO".equalsIgnoreCase(stringOrNull(rs, "IS_NULLABLE")),
|
||||
stringOrNull(rs, "COLUMN_DEFAULT"),
|
||||
primaryKeys.contains(name.toUpperCase(Locale.ROOT)),
|
||||
null,
|
||||
null,
|
||||
intOrNull(rs, "NUMERIC_PRECISION"),
|
||||
intOrNull(rs, "NUMERIC_SCALE"),
|
||||
intOrNull(rs, "CHARACTER_MAXIMUM_LENGTH")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
return columns;
|
||||
});
|
||||
}
|
||||
|
||||
static List<String> dedupeCaseInsensitiveSchemas(List<String> schemas) {
|
||||
Map<String, String> byNormalizedName = new LinkedHashMap<>();
|
||||
for (String schema : schemas) {
|
||||
if (schema == null || schema.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String name = schema.trim();
|
||||
byNormalizedName.putIfAbsent(name.toUpperCase(Locale.ROOT), name);
|
||||
}
|
||||
return new ArrayList<>(byNormalizedName.values());
|
||||
}
|
||||
|
||||
private static Set<String> irisPrimaryKeys(Connection conn, String schema, String table) {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
try (PreparedStatement stmt = conn.prepareStatement(primaryKeySql(schema))) {
|
||||
int index = 1;
|
||||
if (!isBlank(schema)) {
|
||||
stmt.setString(index++, schema.trim());
|
||||
}
|
||||
stmt.setString(index, table);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
String column = rs.getString("COLUMN_NAME");
|
||||
if (!isBlank(column)) {
|
||||
keys.add(column.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// Primary key metadata is optional for display; column listing must keep working.
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static String columnSql(String schema) {
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, "
|
||||
+ "NUMERIC_PRECISION, NUMERIC_SCALE, CHARACTER_MAXIMUM_LENGTH "
|
||||
+ "FROM INFORMATION_SCHEMA.COLUMNS WHERE "
|
||||
);
|
||||
if (!isBlank(schema)) {
|
||||
sql.append("UPPER(TABLE_SCHEMA) = UPPER(?) AND ");
|
||||
}
|
||||
sql.append("UPPER(TABLE_NAME) = UPPER(?) ORDER BY ORDINAL_POSITION");
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
private static String primaryKeySql(String schema) {
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT k.COLUMN_NAME "
|
||||
+ "FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE k "
|
||||
+ "WHERE k.CONSTRAINT_TYPE = 'PRIMARY KEY' AND "
|
||||
);
|
||||
if (!isBlank(schema)) {
|
||||
sql.append("UPPER(k.TABLE_SCHEMA) = UPPER(?) AND ");
|
||||
}
|
||||
sql.append("UPPER(k.TABLE_NAME) = UPPER(?) ORDER BY k.ORDINAL_POSITION");
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
private static String stringOrDefault(ResultSet rs, String column, String fallback) throws Exception {
|
||||
String value = stringOrNull(rs, column);
|
||||
return isBlank(value) ? fallback : value;
|
||||
}
|
||||
|
||||
private static String stringOrNull(ResultSet rs, String column) throws Exception {
|
||||
String value = rs.getString(column);
|
||||
return rs.wasNull() ? null : value;
|
||||
}
|
||||
|
||||
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 boolean isBlank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
new JsonRpcServer(new IrisAgent()).run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.dbx.agent.iris;
|
||||
|
||||
import com.dbx.agent.ColumnInfo;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class IrisAgentTest {
|
||||
@Test
|
||||
void skipsSchemaSwitchingBecauseIrisRejectsSetSchemaContext() {
|
||||
IrisAgent agent = new IrisAgent();
|
||||
|
||||
assertEquals("", agent.setSchemaSQL("Ens"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dedupesSchemasCaseInsensitively() {
|
||||
assertEquals(
|
||||
Arrays.asList("APP", "SQLUSER", "z_user"),
|
||||
IrisAgent.dedupeCaseInsensitiveSchemas(Arrays.asList("APP", "SQLUSER", "SQLUser", "app", "z_user"))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresBlankSchemasWhenDeduping() {
|
||||
assertEquals(
|
||||
Collections.singletonList("SQLUSER"),
|
||||
IrisAgent.dedupeCaseInsensitiveSchemas(Arrays.asList("", " ", null, "SQLUSER"))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsColumnsWithNativeInformationSchemaQueries() {
|
||||
List<String> sql = new ArrayList<>();
|
||||
Connection conn = fakeConnection(sql);
|
||||
|
||||
List<ColumnInfo> columns = IrisAgent.irisColumns(conn, "SQLUser", "People");
|
||||
|
||||
assertEquals(2, columns.size());
|
||||
assertEquals("ID", columns.get(0).getName());
|
||||
assertEquals("INTEGER", columns.get(0).getData_type());
|
||||
assertFalse(columns.get(0).getIs_nullable());
|
||||
assertTrue(columns.get(0).getIs_primary_key());
|
||||
assertEquals("NAME", columns.get(1).getName());
|
||||
assertEquals(Integer.valueOf(64), columns.get(1).getCharacter_maximum_length());
|
||||
assertFalse(sql.get(0).contains("LIMIT"));
|
||||
assertTrue(sql.get(0).contains("INFORMATION_SCHEMA.KEY_COLUMN_USAGE"));
|
||||
assertTrue(sql.get(1).contains("INFORMATION_SCHEMA.COLUMNS"));
|
||||
}
|
||||
|
||||
private static Connection fakeConnection(List<String> sqlLog) {
|
||||
return proxy(Connection.class, (method, args) -> {
|
||||
if ("prepareStatement".equals(method.getName())) {
|
||||
String sql = (String) args[0];
|
||||
sqlLog.add(sql);
|
||||
return fakeStatement(sql);
|
||||
}
|
||||
if ("getMetaData".equals(method.getName())) {
|
||||
throw new AssertionError("IRIS columns must not use JDBC DatabaseMetaData.getColumns()");
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
}
|
||||
|
||||
private static PreparedStatement fakeStatement(String sql) {
|
||||
return proxy(PreparedStatement.class, (method, args) -> {
|
||||
if ("executeQuery".equals(method.getName())) {
|
||||
if (sql.contains("KEY_COLUMN_USAGE")) {
|
||||
return fakeResultSet(List.of(Map.of("COLUMN_NAME", "ID")));
|
||||
}
|
||||
return fakeResultSet(List.of(
|
||||
Map.of(
|
||||
"COLUMN_NAME", "ID",
|
||||
"DATA_TYPE", "INTEGER",
|
||||
"IS_NULLABLE", "NO",
|
||||
"NUMERIC_PRECISION", 10
|
||||
),
|
||||
Map.of(
|
||||
"COLUMN_NAME", "NAME",
|
||||
"DATA_TYPE", "VARCHAR",
|
||||
"IS_NULLABLE", "YES",
|
||||
"CHARACTER_MAXIMUM_LENGTH", 64
|
||||
)
|
||||
));
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
}
|
||||
|
||||
private static ResultSet fakeResultSet(List<Map<String, Object>> rows) {
|
||||
final int[] index = {-1};
|
||||
final boolean[] wasNull = {false};
|
||||
return proxy(ResultSet.class, (method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("next".equals(name)) {
|
||||
index[0] += 1;
|
||||
return index[0] < rows.size();
|
||||
}
|
||||
if ("getString".equals(name)) {
|
||||
Object value = rows.get(index[0]).get((String) args[0]);
|
||||
wasNull[0] = value == null;
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
if ("getObject".equals(name)) {
|
||||
Object value = rows.get(index[0]).get((String) args[0]);
|
||||
wasNull[0] = value == null;
|
||||
return value;
|
||||
}
|
||||
if ("wasNull".equals(name)) {
|
||||
return wasNull[0];
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T proxy(Class<T> type, MethodHandler handler) {
|
||||
InvocationHandler invocationHandler = new InvocationHandler() {
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
return handler.handle(method, args == null ? new Object[0] : args);
|
||||
}
|
||||
};
|
||||
return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, invocationHandler);
|
||||
}
|
||||
|
||||
private static Object defaultValue(Class<?> type) {
|
||||
if (Boolean.TYPE.equals(type)) {
|
||||
return false;
|
||||
}
|
||||
if (Integer.TYPE.equals(type)) {
|
||||
return 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private interface MethodHandler {
|
||||
Object handle(Method method, Object[] args) throws Throwable;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user