mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(grid): enable table data editing and add-row for Cache / IRIS
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+7
-1
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -30,7 +30,7 @@ capabilities:
|
||||
objectSource: true
|
||||
schemaSearch: true
|
||||
diagram: true
|
||||
tableDataEdit: false
|
||||
tableDataEdit: true
|
||||
tableStructureEdit: true
|
||||
tableImport: false
|
||||
dataTransfer: true
|
||||
|
||||
Reference in New Issue
Block a user