Skip to content

Commit 7aac811

Browse files
Add Explicit \N NULL Marker for CSV Export/Import of TEXT Fields (#3199)
Co-authored-by: Peckstadt Yves <[email protected]>
1 parent fabbd68 commit 7aac811

File tree

5 files changed

+68
-2
lines changed

5 files changed

+68
-2
lines changed

data-loader/core/src/main/java/com/scalar/db/dataloader/core/Constants.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,12 @@ public class Constants {
1616
*/
1717
public static final String ABORT_TRANSACTION_STATUS =
1818
"Transaction aborted as part of batch transaction aborted";
19+
/**
20+
* Special null value representation used for TEXT data type columns in CSV files.
21+
*
22+
* <p>This value is used to distinguish between an empty string and a null value in CSV exports
23+
* and imports. When exporting, null TEXT values are converted to this string. When importing,
24+
* this string is converted back to null for TEXT columns.
25+
*/
26+
public static final String CSV_TEXT_NULL_VALUE = "\\N";
1927
}

data-loader/core/src/main/java/com/scalar/db/dataloader/core/dataexport/producer/CsvProducerTask.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.scalar.db.api.Result;
44
import com.scalar.db.api.TableMetadata;
5+
import com.scalar.db.dataloader.core.Constants;
56
import com.scalar.db.dataloader.core.DataLoaderError;
67
import com.scalar.db.dataloader.core.util.CsvUtil;
78
import com.scalar.db.dataloader.core.util.DecimalUtil;
@@ -118,6 +119,11 @@ private String convertResultToCsv(Result result) {
118119
*/
119120
private String convertToString(Result result, String columnName, DataType dataType) {
120121
if (result.isNull(columnName)) {
122+
// Special null value is added when a column of text data type has null value. This is only
123+
// converted for CSV files
124+
if (dataType.equals(DataType.TEXT)) {
125+
return Constants.CSV_TEXT_NULL_VALUE;
126+
}
121127
return null;
122128
}
123129
String value = "";

data-loader/core/src/main/java/com/scalar/db/dataloader/core/util/ColumnUtils.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.scalar.db.api.Result;
55
import com.scalar.db.api.TableMetadata;
66
import com.scalar.db.dataloader.core.ColumnInfo;
7+
import com.scalar.db.dataloader.core.Constants;
78
import com.scalar.db.dataloader.core.DataLoaderError;
89
import com.scalar.db.dataloader.core.exception.Base64Exception;
910
import com.scalar.db.dataloader.core.exception.ColumnParsingException;
@@ -83,8 +84,14 @@ public static Column<?> createColumnFromValue(
8384
DataType dataType, ColumnInfo columnInfo, @Nullable String value)
8485
throws ColumnParsingException {
8586
String columnName = columnInfo.getColumnName();
86-
if (value != null && !dataType.equals(DataType.TEXT) && value.equalsIgnoreCase("null")) {
87-
value = null;
87+
if (value != null) {
88+
if (dataType.equals(DataType.TEXT)) {
89+
if (Constants.CSV_TEXT_NULL_VALUE.equals(value)) {
90+
value = null;
91+
}
92+
} else if (value.equalsIgnoreCase("null")) {
93+
value = null;
94+
}
8895
}
8996
try {
9097
switch (dataType) {

data-loader/core/src/test/java/com/scalar/db/dataloader/core/dataexport/producer/CsvProducerTaskTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import com.scalar.db.api.Result;
44
import com.scalar.db.api.TableMetadata;
55
import com.scalar.db.common.ResultImpl;
6+
import com.scalar.db.dataloader.core.Constants;
67
import com.scalar.db.dataloader.core.UnitTestUtils;
78
import com.scalar.db.io.Column;
89
import com.scalar.db.io.DataType;
10+
import com.scalar.db.io.TextColumn;
911
import java.util.ArrayList;
1012
import java.util.Collections;
1113
import java.util.List;
@@ -116,4 +118,31 @@ void process_withValidResultList_withPartialProjectionsAndMetadata_shouldReturnV
116118
String output = csvProducerTask.process(resultList);
117119
Assertions.assertEquals(expectedOutput, output.trim());
118120
}
121+
122+
@Test
123+
void
124+
process_withValidResultListWithTextFieldWithNullValue_shouldReturnValidCsvStringWithCustomNullValueForTextField() {
125+
String expectedOutput =
126+
"9007199254740992,2147483647,true,0.000000000000000000000000000000000000000000001401298464324817,0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000049,"
127+
+ Constants.CSV_TEXT_NULL_VALUE
128+
+ ",YmxvYiB0ZXN0IHZhbHVl,2000-01-01,01:01:01,2000-01-01T01:01,1970-01-21T03:20:41.740Z,0.000000000000000000000000000000000000000000001401298464324817,0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000049,test value,YmxvYiB0ZXN0IHZhbHVl,txt value 464654654,2147483647,2147483647,9007199254740992,9007199254740992,test value,2147483647,2147483647,9007199254740992,9007199254740992";
129+
Map<String, Column<?>> values = UnitTestUtils.createTestValues();
130+
String textColName = "col6";
131+
Column<?> col = values.get(textColName);
132+
if (col instanceof TextColumn) {
133+
values.put(textColName, TextColumn.of(textColName, null));
134+
}
135+
Result result = new ResultImpl(values, mockMetadata);
136+
List<Result> resultList = new ArrayList<>();
137+
resultList.add(result);
138+
String output = csvProducerTask.process(resultList);
139+
Assertions.assertEquals(expectedOutput, output.trim());
140+
141+
values.put(textColName, TextColumn.ofNull(textColName));
142+
result = new ResultImpl(values, mockMetadata);
143+
resultList = new ArrayList<>();
144+
resultList.add(result);
145+
output = csvProducerTask.process(resultList);
146+
Assertions.assertEquals(expectedOutput, output.trim());
147+
}
119148
}

data-loader/core/src/test/java/com/scalar/db/dataloader/core/util/ColumnUtilsTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.scalar.db.api.TableMetadata;
99
import com.scalar.db.common.ResultImpl;
1010
import com.scalar.db.dataloader.core.ColumnInfo;
11+
import com.scalar.db.dataloader.core.Constants;
1112
import com.scalar.db.dataloader.core.DataLoaderError;
1213
import com.scalar.db.dataloader.core.UnitTestUtils;
1314
import com.scalar.db.dataloader.core.exception.Base64Exception;
@@ -309,4 +310,19 @@ void createColumnFromValue_valueIsNullString_shouldRemainLiteralForTextType()
309310
textCol = ColumnUtils.createColumnFromValue(DataType.TEXT, columnInfo, "nuLL");
310311
assertEquals(TextColumn.of(columnName, "nuLL"), textCol);
311312
}
313+
314+
/**
315+
* Tests that when the string value has custom null value is provided for TEXT columns, it is
316+
* treated as an actual null value
317+
*/
318+
@Test
319+
void createColumnFromValue_customNullValueForText_shouldBeConvertedToNull()
320+
throws ColumnParsingException {
321+
String columnName = "textColumn";
322+
ColumnInfo columnInfo = ColumnInfo.builder().columnName(columnName).build();
323+
324+
Column<?> textCol =
325+
ColumnUtils.createColumnFromValue(DataType.TEXT, columnInfo, Constants.CSV_TEXT_NULL_VALUE);
326+
assertEquals(TextColumn.ofNull(columnName), textCol);
327+
}
312328
}

0 commit comments

Comments
 (0)