什么是Calcite
按照我自己的理解,Apache是一个SQL引擎,通过它,我们可以对各种数据源进行sql化的查询,比如说elasticsearch、file、redis等等。
1 Apache Calcite是什么
首先我们来看下官网对calcite的描述
Apache Calcite is a dynamic data management framework.
It contains many of the pieces that comprise a typical database management system, but omits some key functions: storage of data, algorithms to process data, and a repository for storing metadata.
Calcite intentionally stays out of the business of storing and processing data. As we shall see, this makes it an excellent choice for mediating between applications and one or more data storage locations and data processing engines. It is also a perfect foundation for building a database: just add data.
如果大家不想看那么长串的英文,那我就通过自己的理解来给大家总结一下上面这些话:
Apache Calcite是一个动态的数据管理的框架,内置了很多的数据库系统的管理,比如说ES、Redis、CSV、FileSystem等。但是它并不参与存储,也就是说,Calcite只参与数据的处理。
正是因为它不参与存储,calcite成为了一个通用的不同存储之间的沟通媒介。如果你需要使用Apache Calcite对某个数据库进行处理,那么你唯一要做的就是往Calcite中添加数据即可。
2 例子
下面我们将带领大家来看一个官网的例子:
从CSV中读取数据:
首先github下载源代码 https://github.com/apache/calcite.git
cd calcite/example/csv
./sqlline
sqlline> !connect jdbc:calcite:model=src/test/resources/model.json admin admin
即可进入sql模式.
那么,我们进入model.json文件来一探究竟:
{
"version": "1.0",
"defaultSchema": "SALES",
"schemas": [
{
"name": "SALES",
"type": "custom",
"factory": "org.apache.calcite.adapter.csv.CsvSchemaFactory",
"operand": {
"directory": "sales"
}
}
]
}
读取该文件
那么在哪里读取的这个json呢,ModelHandler这个里面会读取对应的json,然后创建对应的factory,代码在visit()方法中,如下:
public void visit(JsonCustomSchema jsonSchema) {
try {
final SchemaPlus parentSchema = currentMutableSchema("sub-schema");
// 创建org.apache.calcite.adapter.csv.CsvSchemaFactory
final SchemaFactory schemaFactory =
AvaticaUtils.instantiatePlugin(SchemaFactory.class,
jsonSchema.factory);
// 生成CsvSchema
final Schema schema =
schemaFactory.create(
parentSchema, jsonSchema.name, operandMap(jsonSchema, jsonSchema.operand));
final SchemaPlus schemaPlus = parentSchema.add(jsonSchema.name, schema);
populateSchema(jsonSchema, schemaPlus);
} catch (Exception e) {
throw new RuntimeException("Error instantiating " + jsonSchema, e);
}
}
下面我们看org.apache.calcite.adapter.csv.CsvSchemaFactory这个类,这个类有一个create方法,这个方法会返回一个schema,我们看之前的visit方法,里面有调用该方法去生成一个schema。
CsvSchema
这个类继承了AbstractSchema,重写了getTableMap方法,该方法会返回对应的table名称。
该方法调用了createTableMap方法,实际上是从文件中读取数据生成table,接下来我们我们只需要继续看table实现即可。
CsvScannableTable
我们首先举一个最简单的例子,即CsvScannableTable,它继承了CsvTable,实现了ScanableTable
public class CsvScannableTable extends CsvTable
implements ScannableTable {
...
}
我们先看它的父类,CsvTable,需要关注两个方法,第一个是getRowType,第二个是getFieldTypes。
首先我们看getRowType这个方法是重载的我们的父类方法,这个方法的用途是需要返回表的字段名,及其类型。
本质上,这两个方法实际上都调用了CsvEnumerator.deduceRowType这个方法,一个返回的是RelDataType,一个是赋值fieldTypes。
// 获取表的字段名及其类型。
@Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
if (protoRowType != null) {
return protoRowType.apply(typeFactory);
}
if (rowType == null) {
rowType = CsvEnumerator.deduceRowType((JavaTypeFactory) typeFactory, source,
null, isStream());
}
return rowType;
}
// 返回表的字段的类型。
public List<RelDataType> getFieldTypes(RelDataTypeFactory typeFactory) {
if (fieldTypes == null) {
fieldTypes = new ArrayList<>();
CsvEnumerator.deduceRowType((JavaTypeFactory) typeFactory, source,
fieldTypes, isStream());
}
return fieldTypes;
}
CsvEnumerator.deduceRowType
接下来我们看CsvEnumerator.deduceRowType这个方法,别看这个方法很长,实际上做的事情很简单:
- 从csv中读取第一行,获取字符串
- 通过获取的数据,判断类型,构建出fieldType
- 创建struct类型的RelDataType并返回。
- 如果传入了fieldTypes,则把类型添加到这个list中。
public static RelDataType deduceRowType(JavaTypeFactory typeFactory,
Source source, @Nullable List<RelDataType> fieldTypes, Boolean stream) {
final List<RelDataType> types = new ArrayList<>();
final List<String> names = new ArrayList<>();
if (stream) {
names.add(FileSchemaFactory.ROWTIME_COLUMN_NAME);
types.add(typeFactory.createSqlType(SqlTypeName.TIMESTAMP));
}
try (CSVReader reader = openCsv(source)) {
String[] strings = reader.readNext();
if (strings == null) {
strings = new String[]{"EmptyFileHasNoColumns:boolean"};
}
for (String string : strings) {
final String name;
final RelDataType fieldType;
final int colon = string.indexOf(':');
if (colon >= 0) {
name = string.substring(0, colon);
String typeString = string.substring(colon + 1);
Matcher decimalMatcher = DECIMAL_TYPE_PATTERN.matcher(typeString);
if (decimalMatcher.matches()) {
int precision = Integer.parseInt(decimalMatcher.group(1));
int scale = Integer.parseInt(decimalMatcher.group(2));
fieldType = parseDecimalSqlType(typeFactory, precision, scale);
} else {
switch (typeString) {
case "string":
fieldType = toNullableRelDataType(typeFactory, SqlTypeName.VARCHAR);
... 省略代码
default:
LOGGER.warn(
"Found unknown type: {} in file: {} for column: {}. Will assume the type of "
+ "column is string.",
typeString, source.path(), name);
fieldType = toNullableRelDataType(typeFactory, SqlTypeName.VARCHAR);
break;
}
}
} else {
name = string;
fieldType = typeFactory.createSqlType(SqlTypeName.VARCHAR);
}
names.add(name);
types.add(fieldType);
// 如果传入了fieldTypes,则添加进去
if (fieldTypes != null) {
fieldTypes.add(fieldType);
}
}
} catch (IOException e) {
// ignore
}
if (names.isEmpty()) {
names.add("line");
types.add(typeFactory.createSqlType(SqlTypeName.VARCHAR));
}
return typeFactory.createStructType(Pair.zip(names, types));
}
接下来我们把目光返回到CsvScannableTable,看scan方法,这个方法需要返回一个Enumerable的类型。
其中有一个arrayConverter,这个类型,这个是
@Override public Enumerable<@Nullable Object[]> scan(DataContext root) {
JavaTypeFactory typeFactory = root.getTypeFactory();
// 调用的父类的之前getFieldTypes,我们上面第四点已经说过,返回的是表的字段的类型。
final List<RelDataType> fieldTypes = getFieldTypes(typeFactory);
final List<Integer> fields = ImmutableIntList.identity(fieldTypes.size());
final AtomicBoolean cancelFlag = DataContext.Variable.CANCEL_FLAG.get(root);
return new AbstractEnumerable<@Nullable Object[]>() {
@Override public Enumerator<@Nullable Object[]> enumerator() {
return new CsvEnumerator<>(source, cancelFlag, false, null,
CsvEnumerator.arrayConverter(fieldTypes, fields, false));
}
};
}
我们上面CsvEnumerator.deduceRowType 中,有个fieldTypes,就是这个fieldTypes,这个fieldTypes是用来对数据进行转换的。具体使用在ArrayRowConverter
ArrayRowConverter
convert函数
通过sqlType,转换该行对应的字段到对应的类型。
convertRow函数
list调用convert函数。
具体调用在CsvEnumerator中的moveNext()函数,而moveNext函数则在sql执行的时候,获取数据的时候调用。所以在获取数据的时候,会调用convert函数去自适应对应的字段,比如说数据里面是string,但是sql定义的时候是int,则会去尝试转换。
Driver
关于Driver这个类,实际上是实现了jdbc的driver类,具体是怎么实现的,如果后面有时间,我会出一篇文章。