Skip to content

Commit eb7a2c6

Browse files
authored
Merge pull request #5657 from nickgros/PLFM-9706
PLFM-9706 - Fix scalar-to-list null conversion
2 parents 20153d9 + 5f7c7d9 commit eb7a2c6

3 files changed

Lines changed: 76 additions & 5 deletions

File tree

lib/lib-table-cluster/src/main/java/org/sagebionetworks/table/cluster/SQLUtils.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -952,9 +952,13 @@ private static String createSetListColumnFromNonListColumnSql(ColumnChangeDetail
952952
builder.append(tableName);
953953
builder.append(" SET ");
954954
appendColumnNameForId(change.getNewColumn().getId(), builder);
955-
builder.append(" = JSON_ARRAY(");
955+
// Use IF to preserve SQL NULL as NULL: JSON_ARRAY(NULL) produces [null] not NULL,
956+
// which ListStringParser rejects with "null value is not allowed".
957+
builder.append(" = IF(");
956958
appendColumnNameForId(change.getOldColumn().getId(), builder);
957-
builder.append(")");
959+
builder.append(" IS NULL, NULL, JSON_ARRAY(");
960+
appendColumnNameForId(change.getOldColumn().getId(), builder);
961+
builder.append("))");
958962
return builder.toString();
959963
}
960964

lib/lib-table-cluster/src/test/java/org/sagebionetworks/table/cluster/SQLUtilsTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ public void testCreateAlterTableSqlWithListChangeAndAlterTempTrue(){
903903
"ALTER TABLE TEMPT999 ADD COLUMN _C456_ JSON DEFAULT NULL COMMENT 'BOOLEAN_LIST',"
904904
+ " ADD CONSTRAINT CHECK (JSON_SCHEMA_VALID('"
905905
+ "{ \"type\": \"array\", \"items\": { \"maxLength\": null }, \"maxItems\": null }', _C456_))",
906-
"UPDATE TEMPT999 SET _C456_ = JSON_ARRAY(_C123_)",
906+
"UPDATE TEMPT999 SET _C456_ = IF(_C123_ IS NULL, NULL, JSON_ARRAY(_C123_))",
907907
"ALTER TABLE TEMPT999 DROP COLUMN _C123_"
908908
};
909909
// call under test
@@ -942,7 +942,7 @@ public void testCreateAlterTableSqlWithListChangeAndAlterTempFalse(){
942942
"ALTER TABLE T999 ADD COLUMN _C456_ JSON DEFAULT NULL COMMENT 'BOOLEAN_LIST',"
943943
+ " ADD CONSTRAINT CHECK (JSON_SCHEMA_VALID('"
944944
+ "{ \"type\": \"array\", \"items\": { \"maxLength\": null }, \"maxItems\": null }', _C456_))",
945-
"UPDATE T999 SET _C456_ = JSON_ARRAY(_C123_)",
945+
"UPDATE T999 SET _C456_ = IF(_C123_ IS NULL, NULL, JSON_ARRAY(_C123_))",
946946
"ALTER TABLE T999 DROP COLUMN _C123_"
947947
};
948948
// call under test
@@ -2863,7 +2863,7 @@ public void testCreateAlterToListColumnTypeSql() {
28632863
assertEquals("ALTER TABLE T999 ADD COLUMN _C456_ JSON DEFAULT NULL COMMENT 'BOOLEAN_LIST',"
28642864
+ " ADD CONSTRAINT CHECK (JSON_SCHEMA_VALID('"
28652865
+ "{ \"type\": \"array\", \"items\": { \"maxLength\": null }, \"maxItems\": null }', _C456_))", results.get(0));
2866-
assertEquals("UPDATE T999 SET _C456_ = JSON_ARRAY(_C123_)", results.get(1));
2866+
assertEquals("UPDATE T999 SET _C456_ = IF(_C123_ IS NULL, NULL, JSON_ARRAY(_C123_))", results.get(1));
28672867
assertEquals("ALTER TABLE T999 DROP COLUMN _C123_", results.get(2));
28682868
}
28692869

services/workers/src/test/java/org/sagebionetworks/table/worker/TableWorkerIntegrationTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.json.JSONException;
3737
import org.json.JSONObject;
3838
import org.junit.jupiter.api.AfterEach;
39+
import org.junit.jupiter.api.Assertions;
3940
import org.junit.jupiter.api.BeforeEach;
4041
import org.junit.jupiter.api.Disabled;
4142
import org.junit.jupiter.api.Test;
@@ -3585,6 +3586,72 @@ public void testTextMatchesWithSearchEnabledOnSchemaChange() throws Exception {
35853586
});
35863587
}
35873588

3589+
@Test
3590+
public void testSchemaChangeFromEntityIdToEntityIdListWithNullCell() throws Exception {
3591+
// PLFM-9706: converting ENTITYID -> ENTITYID_LIST when a row has a null cell
3592+
// caused the column to be unqueryable because at conversion-time, JSON_ARRAY(NULL)
3593+
// produces [null], which ListStringParser rejects during the index rebuild.
3594+
//
3595+
// If MySQL full text search is enabled on the table, it also enters the PROCESSING_FAILED
3596+
// state for the same reason.
3597+
schema = Lists.newArrayList(
3598+
columnManager.createColumnModel(adminUserInfo, new ColumnModel().setColumnType(ColumnType.STRING).setName("string")),
3599+
columnManager.createColumnModel(adminUserInfo, new ColumnModel().setColumnType(ColumnType.ENTITYID).setName("id"))
3600+
);
3601+
3602+
headers = TableModelUtils.getIds(schema);
3603+
3604+
// Search must be enabled for processing to fail, otherwise it just fails at query-time
3605+
tableId = asyncHelper.createTable(adminUserInfo, UUID.randomUUID().toString(), projectId, headers, true).getId();
3606+
3607+
// One row with a real entity-id value, one row with a null cell.
3608+
List<Row> rows = Arrays.asList(
3609+
TableModelTestUtils.createRow(null, null, "valuePresent", (String) "syn123"),
3610+
TableModelTestUtils.createRow(null, null, "valueAbsent", (String) null)
3611+
);
3612+
3613+
RowSet rowSet = new RowSet();
3614+
rowSet.setRows(rows);
3615+
rowSet.setHeaders(TableModelUtils.getSelectColumns(schema));
3616+
rowSet.setTableId(tableId);
3617+
3618+
referenceSet = appendRows(adminUserInfo, tableId, rowSet, mockProgressCallback);
3619+
3620+
assertEquals(TableState.AVAILABLE, waitForTableProcessing(tableId).getState());
3621+
3622+
// Change the column type from ENTITYID to ENTITYID_LIST via a TableUpdateTransactionRequest.
3623+
ColumnModel idListColumn = columnManager.createColumnModel(adminUserInfo,
3624+
new ColumnModel().setColumnType(ColumnType.ENTITYID_LIST).setName("id"));
3625+
3626+
ColumnChange idColumnChange = new ColumnChange();
3627+
idColumnChange.setOldColumnId(schema.get(1).getId());
3628+
idColumnChange.setNewColumnId(idListColumn.getId());
3629+
3630+
TableSchemaChangeRequest schemaChangeRequest = new TableSchemaChangeRequest();
3631+
schemaChangeRequest.setChanges(Collections.singletonList(idColumnChange));
3632+
schemaChangeRequest.setEntityId(tableId);
3633+
3634+
TableUpdateTransactionRequest transactionRequest = new TableUpdateTransactionRequest();
3635+
transactionRequest.setChanges(Collections.singletonList((TableUpdateRequest) schemaChangeRequest));
3636+
transactionRequest.setEntityId(tableId);
3637+
3638+
// call under test
3639+
asyncHelper.assertJobResponse(adminUserInfo, transactionRequest, Assertions::assertNotNull, MAX_WAIT_MS);
3640+
3641+
// The table must reach AVAILABLE, not PROCESSING_FAILED.
3642+
assertEquals(TableState.AVAILABLE, waitForTableProcessing(tableId).getState());
3643+
3644+
3645+
// Verify the data round-trips correctly: the non-null row should have a
3646+
// single-element list and the null row should remain null.
3647+
waitForConsistentQuery(adminUserInfo, "select * from " + tableId + " order by row_id", null, null, (queryResult) -> {
3648+
List<Row> resultRows = queryResult.getQueryResults().getRows();
3649+
assertEquals(2, resultRows.size());
3650+
assertEquals("[\"syn123\"]", resultRows.get(0).getValues().get(1), "expected [\"syn123\"] for row with syn123, got: " + resultRows.get(0).getValues().get(1));
3651+
assertNull(resultRows.get(1).getValues().get(1));
3652+
});
3653+
}
3654+
35883655
@Test
35893656
public void testWorkerRunInReadOnlyMode() throws Exception {
35903657
asyncHelper.runInReadOnlyMode(()->{

0 commit comments

Comments
 (0)