fix(grid): enable table data editing and add-row for Cache / IRIS

This commit is contained in:
lxk955
2026-10-12 15:53:50 +08:00
committed by GitHub
parent 860d901115
commit 2bf6af1ff0
9 changed files with 245 additions and 8 deletions
@@ -137,10 +137,62 @@ public final class IrisAgent extends ConfiguredJdbcAgent {
if (!hasPrimaryKey(columns)) {
markPrimaryKeys(columns, irisPrimaryKeys(conn, schema, table));
}
markIdentityColumns(columns, irisIdentityColumns(conn, schema, table));
return columns;
});
}
/**
* IRIS RowId / IDENTITY columns are system-assigned: their values cannot be
* user-specified on INSERT, so the grid must omit them when saving new rows
* (mirrors how the Dameng agent reports IDENTITY columns). IRIS exposes this
* through INFORMATION_SCHEMA.COLUMNS extension columns (IS_ROWID / IS_IDENTITY,
* not part of the SQL standard). The probe is defensive on purpose: when a
* server build lacks the extension columns the query fails and column listing
* keeps working exactly as before.
*/
private static Set<String> irisIdentityColumns(Connection conn, String schema, String table) {
Set<String> names = new LinkedHashSet<>();
try (PreparedStatement stmt = conn.prepareStatement(identityColumnSql(schema))) {
int index = 1;
if (!isBlank(schema)) {
stmt.setString(index++, schema.trim());
}
stmt.setString(index, table);
try (ResultSet rs = stmt.executeQuery()) {
addPrimaryKeys(names, rs);
}
} catch (Exception ignored) {
// Identity metadata is optional; older builds report it through
// neither extension column and must not break column listing.
}
return names;
}
private static String identityColumnSql(String schema) {
StringBuilder sql = new StringBuilder(
"SELECT COLUMN_NAME "
+ "FROM INFORMATION_SCHEMA.COLUMNS WHERE "
+ "(UPPER(IS_ROWID) = 'YES' OR UPPER(IS_IDENTITY) = 'YES') AND "
);
if (!isBlank(schema)) {
sql.append("UPPER(TABLE_SCHEMA) = UPPER(?) AND ");
}
sql.append("UPPER(TABLE_NAME) = UPPER(?)");
return sql.toString();
}
private static void markIdentityColumns(List<ColumnInfo> columns, Set<String> identityColumns) {
if (identityColumns.isEmpty()) {
return;
}
for (ColumnInfo column : columns) {
if (identityColumns.contains(column.getName().toUpperCase(Locale.ROOT))) {
column.setExtra("identity");
}
}
}
static List<String> dedupeCaseInsensitiveSchemas(List<String> schemas) {
Map<String, String> byNormalizedName = new LinkedHashMap<>();
for (String schema : schemas) {
@@ -219,7 +219,7 @@ class IrisAgentTest {
assertNull(columns.get(1).getNumeric_precision());
assertNull(columns.get(1).getNumeric_scale());
assertEquals(Integer.valueOf(64), columns.get(1).getCharacter_maximum_length());
assertEquals(1, calls.size());
assertEquals(2, calls.size());
assertFalse(calls.get(0).contains("LIMIT"));
assertTrue(calls.get(0).contains("PRIMARY_KEY"));
assertTrue(calls.get(0).contains("DESCRIPTION"));
@@ -242,9 +242,55 @@ class IrisAgentTest {
assertEquals("identifier", columns.get(0).getComment());
assertNull(columns.get(1).getComment());
assertEquals(1, calls.size());
assertEquals(2, calls.size());
assertTrue(calls.get(0).contains("DESCRIPTION"));
assertFalse(calls.get(0).contains("TABLE_SCHEMA"));
assertTrue(calls.get(1).contains("IS_ROWID"));
assertFalse(calls.get(1).contains("TABLE_SCHEMA"));
}
@Test
void marksRowIdColumnsAsIdentityExtra() {
List<String> calls = new ArrayList<>();
Connection conn = fakeConnection(
calls,
true,
Collections.emptyList(),
null,
Collections.emptyList(),
List.of(Map.of("COLUMN_NAME", "ID")),
null
);
List<ColumnInfo> columns = IrisAgent.irisColumns(conn, "SQLUser", "People");
assertEquals("identity", columns.get(0).getExtra());
assertNull(columns.get(1).getExtra());
assertEquals(2, calls.size());
assertTrue(calls.get(1).contains("IS_ROWID"));
assertTrue(calls.get(1).contains("IS_IDENTITY"));
assertTrue(calls.get(1).contains("UPPER(TABLE_SCHEMA) = UPPER(?)"));
}
@Test
void keepsColumnListingWhenIdentityColumnsAreUnavailable() {
List<String> calls = new ArrayList<>();
Connection conn = fakeConnection(
calls,
true,
Collections.emptyList(),
null,
Collections.emptyList(),
null,
new SQLException("Field 'IS_ROWID' not found")
);
List<ColumnInfo> columns = IrisAgent.irisColumns(conn, "SQLUser", "People");
assertEquals(2, columns.size());
assertNull(columns.get(0).getExtra());
assertNull(columns.get(1).getExtra());
assertEquals(2, calls.size());
}
@Test
@@ -261,9 +307,10 @@ class IrisAgentTest {
List<ColumnInfo> columns = IrisAgent.irisColumns(conn, "SQLUser", "People");
assertTrue(columns.get(0).getIs_primary_key());
assertEquals(2, calls.size());
assertEquals(3, calls.size());
assertTrue(calls.get(0).contains("INFORMATION_SCHEMA.COLUMNS"));
assertEquals("JDBC_PRIMARY_KEYS", calls.get(1));
assertTrue(calls.get(2).contains("IS_ROWID"));
}
@Test
@@ -280,9 +327,11 @@ class IrisAgentTest {
List<ColumnInfo> columns = IrisAgent.irisColumns(conn, "SQLUser", "People");
assertTrue(columns.get(0).getIs_primary_key());
assertEquals(4, calls.size());
assertTrue(calls.get(0).contains("INFORMATION_SCHEMA.COLUMNS"));
assertEquals("JDBC_PRIMARY_KEYS", calls.get(1));
assertTrue(calls.get(2).contains("INFORMATION_SCHEMA.KEY_COLUMN_USAGE"));
assertTrue(calls.get(3).contains("IS_ROWID"));
}
@Test
@@ -300,7 +349,9 @@ class IrisAgentTest {
assertFalse(columns.get(0).getIs_primary_key());
assertFalse(columns.get(1).getIs_primary_key());
assertEquals(4, calls.size());
assertTrue(calls.get(2).contains("INFORMATION_SCHEMA.KEY_COLUMN_USAGE"));
assertTrue(calls.get(3).contains("IS_ROWID"));
}
private static Connection fakeConnection(
@@ -309,12 +360,26 @@ class IrisAgentTest {
List<Map<String, Object>> jdbcPrimaryKeys,
SQLException jdbcPrimaryKeyFailure,
List<Map<String, Object>> nativePrimaryKeys
) {
return fakeConnection(
callLog, inlinePrimaryKey, jdbcPrimaryKeys, jdbcPrimaryKeyFailure, nativePrimaryKeys, null, null
);
}
private static Connection fakeConnection(
List<String> callLog,
boolean inlinePrimaryKey,
List<Map<String, Object>> jdbcPrimaryKeys,
SQLException jdbcPrimaryKeyFailure,
List<Map<String, Object>> nativePrimaryKeys,
List<Map<String, Object>> identityColumns,
SQLException identityQueryFailure
) {
return proxy(Connection.class, (method, args) -> {
if ("prepareStatement".equals(method.getName())) {
String sql = (String) args[0];
callLog.add(sql);
return fakeStatement(sql, inlinePrimaryKey, nativePrimaryKeys);
return fakeStatement(sql, inlinePrimaryKey, nativePrimaryKeys, identityColumns, identityQueryFailure);
}
if ("getMetaData".equals(method.getName())) {
return fakeMetadata(callLog, jdbcPrimaryKeys, jdbcPrimaryKeyFailure);
@@ -349,10 +414,18 @@ class IrisAgentTest {
private static PreparedStatement fakeStatement(
String sql,
boolean inlinePrimaryKey,
List<Map<String, Object>> nativePrimaryKeys
List<Map<String, Object>> nativePrimaryKeys,
List<Map<String, Object>> identityColumns,
SQLException identityQueryFailure
) {
return proxy(PreparedStatement.class, (method, args) -> {
if ("executeQuery".equals(method.getName())) {
if (sql.contains("IS_ROWID")) {
if (identityQueryFailure != null) {
throw identityQueryFailure;
}
return fakeResultSet(identityColumns == null ? Collections.emptyList() : identityColumns);
}
if (sql.contains("KEY_COLUMN_USAGE")) {
return fakeResultSet(nativePrimaryKeys);
}
@@ -93,7 +93,7 @@ interface MountGridOptions {
quickEntry?: boolean;
hideNullColumns?: boolean;
readonlyColumnIndexes?: number[];
databaseType?: "dameng" | "mongodb";
databaseType?: "dameng" | "mongodb" | "iris";
customSaveHandler?: CustomSaveHandler;
editable?: boolean;
allowInsertRows?: boolean;
@@ -864,4 +864,10 @@ describe("selected transpose edit scope", () => {
await settle();
expect(transposeCells(host).map((cell) => cell.textContent?.trim())).toEqual(["third", "first"]);
});
it("renders the add-row toolbar action for Caché / IRIS table data (#11808)", async () => {
const { host } = mountGrid({ databaseType: "iris" });
await settle();
expect(host.querySelector("[data-toolbar-action='addRow']")).not.toBeNull();
});
});
@@ -72,4 +72,8 @@ describe("databaseDriverManifest", () => {
expect(databaseConnectionFormKind("mqtt")).toBe("mqtt");
expect(databaseConnectionFormKind("nacos")).toBe("nacos");
});
it("advertises tableDataEdit capability for InterSystems IRIS / Caché (#11808)", () => {
expect(supportsDatabaseFeature("iris", "tableDataEdit")).toBe(true);
});
});
@@ -253,4 +253,14 @@ describe("tableEditing", () => {
expect(canDeleteExistingTdengineRows("salesforce", ["Id"])).toBe(true);
});
});
it("supports InterSystems Caché / IRIS table editing and row insertion (#11808)", () => {
expect(isTableDataEditable("iris", ["id"], "TABLE")).toBe(true);
expect(isTableDataEditable("iris", [], "TABLE")).toBe(true);
expect(isTableDataEditable("iris", ["id"], "VIEW")).toBe(false);
expect(canInsertTableRows("iris")).toBe(true);
expect(canEditExistingTableRows("iris")).toBe(true);
expect(supportsDataGridTransaction("iris")).toBe(true);
expect(canUseKeylessRowPredicate("iris", [])).toBe(true);
});
});
@@ -88,6 +88,7 @@ const NAVICAT_STYLE_TABLE_DATA_TYPES = new Set<DatabaseType>([
"sundb",
"oscar",
"databend",
"iris",
]);
const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<DatabaseCapability>>> = {
@@ -2833,7 +2833,7 @@
"objectSource": true,
"schemaSearch": true,
"diagram": true,
"tableDataEdit": false,
"tableDataEdit": true,
"tableStructureEdit": true,
"tableImport": false,
"dataTransfer": true,
+91
View File
@@ -5020,6 +5020,97 @@ mod tests {
);
}
#[test]
fn iris_keyless_save_uses_full_row_predicate() {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Iris),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
schema: Some("SQLUser".to_string()),
table_name: "Unkeyed".to_string(),
primary_keys: vec![],
columns: Some(vec![
column("DocId", "integer", false, None),
column("Title", "varchar(64)", true, None),
]),
},
columns: vec!["DocId".to_string(), "Title".to_string()],
source_columns: None,
rows: vec![vec![json!(7), json!("draft")]],
dirty_rows: vec![(0, vec![(1, json!("final"))])],
deleted_rows: vec![],
new_rows: vec![],
include_database_name: false,
});
assert_eq!(result.validation_error, None);
assert_eq!(
result.statements,
vec!["UPDATE SQLUser.Unkeyed SET Title = 'final' WHERE DocId = 7 AND Title = 'draft';"]
);
}
#[test]
fn iris_keyless_save_rejects_unidentifiable_rows() {
// Mirrors the computed-column case: when no result column maps to a
// table column the row predicate is empty and the save must stay
// disabled instead of emitting a dangling WHERE.
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Iris),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
schema: Some("SQLUser".to_string()),
table_name: "Unkeyed".to_string(),
primary_keys: vec![],
columns: Some(vec![column("DocId", "integer", true, None), column("Title", "varchar(64)", true, None)]),
},
columns: vec!["expr".to_string(), "display".to_string()],
source_columns: Some(vec![None, None]),
rows: vec![vec![json!("x"), json!("draft")]],
dirty_rows: vec![(0, vec![(1, json!("final"))])],
deleted_rows: vec![],
new_rows: vec![],
include_database_name: false,
});
assert!(result.validation_error.is_some(), "{:?}", result.validation_error);
assert!(result.statements.is_empty());
}
#[test]
fn iris_identity_columns_are_omitted_from_new_row_inserts() {
let row_id = column("PAPMI_RowId", "integer", false, Some("identity"));
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Iris),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
schema: Some("SQLUser".to_string()),
table_name: "PA_PatMas".to_string(),
primary_keys: vec![],
columns: Some(vec![row_id, column("PAPMI_ID", "varchar(32)", false, None)]),
},
columns: vec!["PAPMI_RowId".to_string(), "PAPMI_ID".to_string()],
source_columns: None,
rows: vec![],
dirty_rows: vec![],
deleted_rows: vec![],
new_rows: vec![vec![Value::Null, json!("new-id")]],
include_database_name: false,
});
assert_eq!(result.validation_error, None);
assert_eq!(result.statements, vec!["INSERT INTO SQLUser.PA_PatMas (PAPMI_ID) VALUES ('new-id');"]);
}
#[test]
fn iris_data_grid_save_quotes_only_delimited_identifiers_across_mutations() {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
+1 -1
View File
@@ -30,7 +30,7 @@ capabilities:
objectSource: true
schemaSearch: true
diagram: true
tableDataEdit: false
tableDataEdit: true
tableStructureEdit: true
tableImport: false
dataTransfer: true