in baremaps-calcite/src/main/java/org/apache/baremaps/calcite/data/DataTableSchema.java [41:294]
public record DataTableSchema(String name,
List<DataColumn> columns) implements Serializable {
/**
* Constructs a schema with validation.
*
* @param name the name of the schema
* @param columns the columns in the schema
* @throws NullPointerException if name or columns is null
* @throws IllegalArgumentException if name is blank, columns is empty, or columns contains
* duplicates
*/
public DataTableSchema {
Objects.requireNonNull(name, "Schema name cannot be null");
Objects.requireNonNull(columns, "Columns cannot be null");
if (name.isBlank()) {
throw new IllegalArgumentException("Schema name cannot be blank");
}
if (columns.isEmpty()) {
throw new IllegalArgumentException("Columns cannot be empty");
}
// Check for duplicate column names
Set<String> columnNames = new HashSet<>();
for (DataColumn column : columns) {
if (!columnNames.add(column.name())) {
throw new IllegalArgumentException("Duplicate column name: " + column.name());
}
}
// Make defensive copy
columns = List.copyOf(columns);
}
/**
* Creates a new row for this schema with all values set to null.
*
* @return a new row
*/
public DataRow createRow() {
var values = new ArrayList<>(columns.size());
for (int i = 0; i < columns.size(); i++) {
values.add(null);
}
return new DataRow(this, values);
}
/**
* Gets a column by name.
*
* @param name the name of the column
* @return the column
* @throws IllegalArgumentException if the column does not exist
*/
public DataColumn getColumn(String name) {
Objects.requireNonNull(name, "Column name cannot be null");
for (DataColumn column : columns) {
if (column.name().equals(name)) {
return column;
}
}
throw new IllegalArgumentException("Column not found: " + name);
}
/**
* Gets the index of a column by name.
*
* @param name the name of the column
* @return the index of the column
* @throws IllegalArgumentException if the column does not exist
*/
public int getColumnIndex(String name) {
Objects.requireNonNull(name, "Column name cannot be null");
for (int i = 0; i < columns.size(); i++) {
if (columns.get(i).name().equals(name)) {
return i;
}
}
throw new IllegalArgumentException("Column not found: " + name);
}
/**
* Checks if a column exists.
*
* @param name the name of the column
* @return true if the column exists
*/
public boolean hasColumn(String name) {
Objects.requireNonNull(name, "Column name cannot be null");
for (DataColumn column : columns) {
if (column.name().equals(name)) {
return true;
}
}
return false;
}
/**
* Custom JSON deserializer for DataSchema.
*/
static class DataSchemaDeserializer extends JsonDeserializer<DataTableSchema> {
private RelDataTypeFactory typeFactory;
/**
* Constructs a DataSchemaDeserializer with the given type factory.
*
* @param typeFactory the type factory to use
*/
public DataSchemaDeserializer(RelDataTypeFactory typeFactory) {
this.typeFactory = Objects.requireNonNull(typeFactory, "Type factory cannot be null");
}
@Override
public DataTableSchema deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException {
ObjectNode node = parser.getCodec().readTree(parser);
if (!node.has("name")) {
throw new IOException("Missing required field: name");
}
if (!node.has("columns")) {
throw new IOException("Missing required field: columns");
}
String name = node.get("name").asText();
List<DataColumn> columns = new ArrayList<>();
JsonNode columnsNode = node.get("columns");
if (!columnsNode.isArray()) {
throw new IOException("columns field must be an array");
}
columnsNode.elements().forEachRemaining(column -> {
try {
columns.add(deserialize(column));
} catch (Exception e) {
throw new RuntimeException("Error deserializing column", e);
}
});
return new DataTableSchema(name, columns);
}
DataColumn deserialize(JsonNode node) {
if (!node.has("name") || !node.has("cardinality") || !node.has("sqlTypeName")) {
throw new IllegalArgumentException(
"Column is missing required fields: name, cardinality, or sqlTypeName");
}
String columnName = node.get("name").asText();
Cardinality cardinality;
try {
cardinality = Cardinality.valueOf(node.get("cardinality").asText());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Invalid cardinality value: " + node.get("cardinality").asText());
}
SqlTypeName sqlTypeName;
try {
sqlTypeName = SqlTypeName.valueOf(node.get("sqlTypeName").asText());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Invalid SQL type name value: " + node.get("sqlTypeName").asText());
}
// Create the RelDataType based on the SqlTypeName
RelDataType relDataType;
if (sqlTypeName == SqlTypeName.ROW) {
if (!node.has("columns")) {
throw new IllegalArgumentException("Nested column is missing required field: columns");
}
List<DataColumn> columns = new ArrayList<>();
JsonNode columnsNode = node.get("columns");
if (!columnsNode.isArray()) {
throw new IllegalArgumentException("columns field must be an array");
}
columnsNode.elements().forEachRemaining(column -> {
columns.add(deserialize(column));
});
return DataColumnNested.of(columnName, cardinality, columns, typeFactory);
} else {
// Create basic type without nullability, precision, etc.
relDataType = typeFactory.createSqlType(sqlTypeName);
// Handle nullability based on cardinality
if (cardinality == Cardinality.OPTIONAL) {
relDataType = typeFactory.createTypeWithNullability(relDataType, true);
}
return new DataColumnFixed(columnName, cardinality, relDataType);
}
}
}
/**
* Configures an ObjectMapper for DataSchema serialization/deserialization.
*
* @param typeFactory the type factory to use
* @return a configured ObjectMapper
*/
private static ObjectMapper configureObjectMapper(RelDataTypeFactory typeFactory) {
var mapper = new ObjectMapper();
mapper.registerSubtypes(
new NamedType(DataColumnFixed.class, "FIXED"),
new NamedType(DataColumnNested.class, "NESTED"));
var module = new SimpleModule();
module.addDeserializer(DataTableSchema.class, new DataSchemaDeserializer(typeFactory));
mapper.registerModule(module);
return mapper;
}
/**
* Reads a DataSchema from an input stream.
*
* @param inputStream the input stream
* @param typeFactory the type factory to use
* @return the schema
* @throws IOException if an I/O error occurs
*/
public static DataTableSchema read(InputStream inputStream, RelDataTypeFactory typeFactory)
throws IOException {
Objects.requireNonNull(inputStream, "Input stream cannot be null");
Objects.requireNonNull(typeFactory, "Type factory cannot be null");
var mapper = configureObjectMapper(typeFactory);
return mapper.readValue(inputStream, DataTableSchema.class);
}
/**
* Writes a DataSchema to an output stream.
*
* @param outputStream the output stream
* @param schema the schema
* @param typeFactory the type factory to use
* @throws IOException if an I/O error occurs
*/
public static void write(OutputStream outputStream, DataTableSchema schema,
RelDataTypeFactory typeFactory) throws IOException {
Objects.requireNonNull(outputStream, "Output stream cannot be null");
Objects.requireNonNull(schema, "Schema cannot be null");
Objects.requireNonNull(typeFactory, "Type factory cannot be null");
var mapper = configureObjectMapper(typeFactory);
mapper.writeValue(outputStream, schema);
}
}