This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/tsfile.git
The following commit(s) were added to refs/heads/develop by this push:
new ea0945935 Support TsFile properties and optional table point counting
(#882)
ea0945935 is described below
commit ea0945935a9bee5154072a8349bd4df7c9a04143
Author: Jiang Tian <[email protected]>
AuthorDate: Tue Aug 4 12:19:14 2026 +0800
Support TsFile properties and optional table point counting (#882)
* add property read-write interface
* May record table point count
* Add perf test
* Add table point count repair tool
* add point-count tool
* add point-count tool
* supplement usage for other os
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
* supplement usage for other os
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
* use byte[] for properties
* use byte[8] for table point count
---------
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
---
.../org/apache/tsfile/i18n/messages.properties | 7 +
.../org/apache/tsfile/i18n/messages_zh.properties | 7 +
java/tools/README-zh.md | 21 ++
java/tools/README.md | 26 ++
.../resources/tools/tsfile-table-point-count.bat | 49 ++++
.../resources/tools/tsfile-table-point-count.sh | 50 ++++
java/tools/src/assembly/tools.xml | 9 +
.../tsfile/file/metadata/TsFileMetadata.java | 74 +++--
.../apache/tsfile/read/TsFileSequenceReader.java | 5 +
.../tsfile/read/v4/DeviceTableModelReader.java | 6 +
.../org/apache/tsfile/read/v4/ITsFileReader.java | 4 +
.../tsfile/utils/TsFileTablePointCountTool.java | 318 +++++++++++++++++++++
.../write/v4/AbstractTableModelTsFileWriter.java | 6 +
.../org/apache/tsfile/write/v4/ITsFileWriter.java | 3 +
.../apache/tsfile/write/writer/TsFileIOWriter.java | 48 +++-
.../tsfile/file/metadata/TsFileMetadataTest.java | 11 +-
.../apache/tsfile/file/metadata/utils/Utils.java | 21 +-
.../read/TsFileV4ReadWriteInterfacesTest.java | 39 +++
.../utils/TsFileTablePointCountToolTest.java | 141 +++++++++
.../write/TablePointCountPerformanceTest.java | 269 +++++++++++++++++
.../apache/tsfile/write/TsFileWriteApiTest.java | 64 +++++
21 files changed, 1142 insertions(+), 36 deletions(-)
diff --git
a/java/common/src/main/resources/org/apache/tsfile/i18n/messages.properties
b/java/common/src/main/resources/org/apache/tsfile/i18n/messages.properties
index 98909f7a6..9f6f35857 100644
--- a/java/common/src/main/resources/org/apache/tsfile/i18n/messages.properties
+++ b/java/common/src/main/resources/org/apache/tsfile/i18n/messages.properties
@@ -1548,3 +1548,10 @@ log.tools.csv_read_error = Error reading CSV file: %1$s
# CsvSourceReader — error closing CSV reader
log.tools.csv_close_reader_error = Error closing CSV reader
+
+# TsFileTablePointCountTool
+error.utils.table_point_count_tool_usage = Usage: TsFileTablePointCountTool
<file.tsfile>
+error.utils.table_point_count_tool_file_not_found = TsFile does not exist: %1$s
+error.utils.table_point_count_tool_incomplete_file = TsFile is incomplete: %1$s
+error.utils.table_point_count_tool_copy_failed = Failed to copy the TsFile
data and metadata prefix
+info.utils.table_point_count_tool_result = Processed %1$s: %2$s
diff --git
a/java/common/src/main/resources/org/apache/tsfile/i18n/messages_zh.properties
b/java/common/src/main/resources/org/apache/tsfile/i18n/messages_zh.properties
index a1a437cfb..17c341af2 100644
---
a/java/common/src/main/resources/org/apache/tsfile/i18n/messages_zh.properties
+++
b/java/common/src/main/resources/org/apache/tsfile/i18n/messages_zh.properties
@@ -1548,3 +1548,10 @@ log.tools.csv_read_error = 读取 CSV 文件出错: %1$s
# CsvSourceReader — error closing CSV reader
log.tools.csv_close_reader_error = 关闭 CSV reader 出错
+
+# TsFileTablePointCountTool
+error.utils.table_point_count_tool_usage = 用法:TsFileTablePointCountTool
<file.tsfile>
+error.utils.table_point_count_tool_file_not_found = TsFile 不存在:%1$s
+error.utils.table_point_count_tool_incomplete_file = TsFile 不完整:%1$s
+error.utils.table_point_count_tool_copy_failed = 复制 TsFile 数据和元数据前缀失败
+info.utils.table_point_count_tool_result = 已处理 %1$s:%2$s
diff --git a/java/tools/README-zh.md b/java/tools/README-zh.md
index 46c615ed0..7ff5ef6e4 100644
--- a/java/tools/README-zh.md
+++ b/java/tools/README-zh.md
@@ -229,3 +229,24 @@ arrow2tsfile.bat --source .\data\arrow --target .\output
--fail_dir .\failed
- 多批次:`{源文件名}_1.tsfile`、`{源文件名}_2.tsfile`、...
- 表名与输出文件名相互独立——表名来自 schema 或 `--table_name`,文件名来自源文件。
+## 表点数统计工具
+
+`tools/tsfile-table-point-count.sh`(Linux/macOS)和
`tools\tsfile-table-point-count.bat`(Windows)用于检查完整 TsFile
是否包含表级点数统计属性,并在属性缺失时补写。每张表的点数是其所有 FIELD 列中非空值的总数,不统计 TAG 列和时间列。
+
+设置 `JAVA_HOME` 后,传入且仅传入一个 TsFile 路径:
+
+ # Linux/macOS
+ tools/tsfile-table-point-count.sh /data/example.tsfile
+
+ :: Windows
+ tools\tsfile-table-point-count.bat C:\data\example.tsfile
+
+工具会输出以下状态之一:
+
+| 状态 | 说明 |
+|------|------|
+| `UPDATED` | 已计算缺失的点数统计属性并写回文件。 |
+| `ALREADY_PRESENT` | 每张表都已有有效的点数统计属性,未修改文件。 |
+| `NO_TABLE` | 文件不包含表 Schema,例如仅含树模型数据的 TsFile,未修改文件。 |
+
+仅当状态为 `UPDATED`
时,工具才会原地修改输入文件。工具先在源文件所在目录生成完整的临时文件,确保重写后的元数据落盘后再替换源文件。运行前请确保目录可写、磁盘空间足以存放临时副本,并备份重要文件。不存在的文件和未写完整的
TsFile 会被拒绝处理。
diff --git a/java/tools/README.md b/java/tools/README.md
index ad1f40215..18c5cd780 100644
--- a/java/tools/README.md
+++ b/java/tools/README.md
@@ -228,3 +228,29 @@ arrow2tsfile.bat --source .\data\arrow --target .\output
--fail_dir .\failed
- Multiple batches: `{source_basename}_1.tsfile`,
`{source_basename}_2.tsfile`, ...
- Table name and output filename are independent — table name comes from
schema or `--table_name`, filename comes from source file.
+## Table Point Count Tool
+
+`tools/tsfile-table-point-count.sh` (Linux/macOS) and
`tools\tsfile-table-point-count.bat` (Windows) check whether a complete TsFile
contains table-level
+point-count properties and backfills them when they are missing. For each
table, the point count is
+the total number of non-null values in its FIELD columns; TAG and time columns
are not counted.
+
+Set `JAVA_HOME`, then run the tool with exactly one TsFile path:
+
+ # Linux/macOS
+ tools/tsfile-table-point-count.sh /data/example.tsfile
+
+ :: Windows
+ tools\tsfile-table-point-count.bat C:\data\example.tsfile
+
+The tool prints one of the following statuses:
+
+| Status | Description |
+|--------|-------------|
+| `UPDATED` | Missing point-count properties were calculated and written to
the file. |
+| `ALREADY_PRESENT` | Every table already had a valid point-count property;
the file was not modified. |
+| `NO_TABLE` | The file contains no table schema, for example a
tree-model-only TsFile; the file was not modified. |
+
+The tool modifies the input file in place only for `UPDATED`. It writes a
complete temporary copy
+next to the source file and replaces the source after the rewritten metadata
has been flushed. Make
+sure the directory is writable, leave enough free space for a temporary copy,
and back up important
+files before running the tool. Missing files and incomplete TsFiles are
rejected.
diff --git
a/java/tools/src/assembly/resources/tools/tsfile-table-point-count.bat
b/java/tools/src/assembly/resources/tools/tsfile-table-point-count.bat
new file mode 100644
index 000000000..a89d6c13d
--- /dev/null
+++ b/java/tools/src/assembly/resources/tools/tsfile-table-point-count.bat
@@ -0,0 +1,49 @@
+@REM
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM
+
+@echo off
+setlocal enabledelayedexpansion
+
+if "%OS%" == "Windows_NT" setlocal
+
+pushd %~dp0..
+if NOT DEFINED TSFILE_HOME set TSFILE_HOME=%CD%
+popd
+
+set JAVA_OPTS=-ea^
+ -DTSFILE_HOME="%TSFILE_HOME%"
+
+if NOT DEFINED JAVA_HOME goto :err
+
+echo ------------------------------------------
+echo Starting TsFile Table Point Count Tool
+echo ------------------------------------------
+
+set CLASSPATH="%TSFILE_HOME%\lib\*"
+set MAIN_CLASS=org.apache.tsfile.utils.TsFileTablePointCountTool
+
+@REM The argument is the TsFile to inspect. The tool rewrites its properties
only when table-level
+@REM point-count statistics are missing.
+"%JAVA_HOME%\bin\java" -DTSFILE_HOME=!TSFILE_HOME! !JAVA_OPTS! -cp !CLASSPATH!
!MAIN_CLASS! %*
+exit /b %ERRORLEVEL%
+
+:err
+echo JAVA_HOME environment variable must be set!
+set ret_code=1
+exit /b %ret_code%
diff --git
a/java/tools/src/assembly/resources/tools/tsfile-table-point-count.sh
b/java/tools/src/assembly/resources/tools/tsfile-table-point-count.sh
new file mode 100644
index 000000000..34c902628
--- /dev/null
+++ b/java/tools/src/assembly/resources/tools/tsfile-table-point-count.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+echo ------------------------------------------
+echo Starting TsFile Table Point Count Tool
+echo ------------------------------------------
+
+if [ -z "${TSFILE_HOME}" ]; then
+ export TSFILE_HOME="$(cd "`dirname "$0"`"/..; pwd)"
+fi
+
+if [ -n "$JAVA_HOME" ]; then
+ for java in "$JAVA_HOME"/bin/amd64/java "$JAVA_HOME"/bin/java; do
+ if [ -x "$java" ]; then
+ JAVA="$java"
+ break
+ fi
+ done
+else
+ JAVA=java
+fi
+
+if [ -z $JAVA ] ; then
+ echo Unable to find java executable. Check JAVA_HOME and PATH environment
variables. > /dev/stderr
+ exit 1;
+fi
+
+CLASSPATH=${TSFILE_HOME}/lib/*
+MAIN_CLASS=org.apache.tsfile.utils.TsFileTablePointCountTool
+
+# The argument is the TsFile to inspect. The tool rewrites its properties only
when table-level
+# point-count statistics are missing.
+exec "$JAVA" -DTSFILE_HOME=${TSFILE_HOME} -cp "$CLASSPATH" "$MAIN_CLASS" "$@"
diff --git a/java/tools/src/assembly/tools.xml
b/java/tools/src/assembly/tools.xml
index 31e9c81a0..b58e8b8e2 100644
--- a/java/tools/src/assembly/tools.xml
+++ b/java/tools/src/assembly/tools.xml
@@ -69,5 +69,14 @@
<source>${maven.multiModuleProjectDirectory}/java/tools/src/assembly/resources/tools/arrow2tsfile.bat</source>
<destName>tools/arrow2tsfile.bat</destName>
</file>
+ <file>
+
<source>${maven.multiModuleProjectDirectory}/java/tools/src/assembly/resources/tools/tsfile-table-point-count.sh</source>
+ <destName>tools/tsfile-table-point-count.sh</destName>
+ <fileMode>0755</fileMode>
+ </file>
+ <file>
+
<source>${maven.multiModuleProjectDirectory}/java/tools/src/assembly/resources/tools/tsfile-table-point-count.bat</source>
+ <destName>tools/tsfile-table-point-count.bat</destName>
+ </file>
</files>
</assembly>
diff --git
a/java/tsfile/src/main/java/org/apache/tsfile/file/metadata/TsFileMetadata.java
b/java/tsfile/src/main/java/org/apache/tsfile/file/metadata/TsFileMetadata.java
index 318728553..7caae06b2 100644
---
a/java/tsfile/src/main/java/org/apache/tsfile/file/metadata/TsFileMetadata.java
+++
b/java/tsfile/src/main/java/org/apache/tsfile/file/metadata/TsFileMetadata.java
@@ -19,6 +19,7 @@
package org.apache.tsfile.file.metadata;
+import org.apache.tsfile.common.conf.TSFileConfig;
import org.apache.tsfile.compatibility.DeserializeConfig;
import org.apache.tsfile.encrypt.EncryptUtils;
import org.apache.tsfile.exception.encrypt.EncryptException;
@@ -45,7 +46,7 @@ public class TsFileMetadata {
private Map<String, MetadataIndexNode> tableMetadataIndexNodeMap;
private Map<String, TableSchema> tableSchemaMap;
private boolean hasTableSchemaMapCache;
- private Map<String, String> tsFileProperties;
+ private Map<String, byte[]> tsFileProperties;
// offset of MetaMarker.SEPARATOR
private long metaOffset;
@@ -117,21 +118,27 @@ public class TsFileMetadata {
if (buffer.hasRemaining()) {
int propertiesSize = ReadWriteForEncodingUtils.readVarInt(buffer);
- Map<String, String> propertiesMap = new HashMap<>();
+ Map<String, byte[]> propertiesMap = new HashMap<>();
for (int i = 0; i < propertiesSize; i++) {
String key = ReadWriteIOUtils.readVarIntString(buffer);
- String value = ReadWriteIOUtils.readVarIntString(buffer);
+ int valueSize = ReadWriteForEncodingUtils.readVarInt(buffer);
+ byte[] value = null;
+ if (valueSize >= 0) {
+ value = new byte[valueSize];
+ buffer.get(value);
+ }
propertiesMap.put(key, value);
}
+ String encryptLevel = getPropertyAsString(propertiesMap, "encryptLevel");
// if the file is not encrypted, set the default value(for compatible
reason)
- if (!propertiesMap.containsKey("encryptLevel") ||
propertiesMap.get("encryptLevel") == null) {
- propertiesMap.put("encryptLevel", "0");
- propertiesMap.put("encryptType",
"org.apache.tsfile.encrypt.UNENCRYPTED");
- propertiesMap.put("encryptKey", "");
- } else if (propertiesMap.get("encryptLevel").equals("0")) {
- propertiesMap.put("encryptType",
"org.apache.tsfile.encrypt.UNENCRYPTED");
- propertiesMap.put("encryptKey", "");
- } else if (propertiesMap.get("encryptLevel").equals("1")) {
+ if (!propertiesMap.containsKey("encryptLevel") || encryptLevel == null) {
+ propertiesMap.put("encryptLevel", stringToBytes("0"));
+ propertiesMap.put("encryptType",
stringToBytes("org.apache.tsfile.encrypt.UNENCRYPTED"));
+ propertiesMap.put("encryptKey", stringToBytes(""));
+ } else if (encryptLevel.equals("0")) {
+ propertiesMap.put("encryptType",
stringToBytes("org.apache.tsfile.encrypt.UNENCRYPTED"));
+ propertiesMap.put("encryptKey", stringToBytes(""));
+ } else if (encryptLevel.equals("1")) {
if (!propertiesMap.containsKey("encryptType")) {
throw new EncryptException(
Messages.format("error.file.tsfile_metadata_no_encrypt_type",
1));
@@ -140,15 +147,15 @@ public class TsFileMetadata {
throw new EncryptException(
Messages.format("error.file.tsfile_metadata_no_encrypt_key", 1));
}
- if (propertiesMap.get("encryptKey") == null ||
propertiesMap.get("encryptKey").isEmpty()) {
+ String encryptKey = getPropertyAsString(propertiesMap, "encryptKey");
+ if (encryptKey == null || encryptKey.isEmpty()) {
throw new EncryptException(
Messages.format("error.file.tsfile_metadata_null_encrypt_key",
1));
}
- String str = propertiesMap.get("encryptKey");
fileMetaData.encryptLevel = 1;
- fileMetaData.secondKey = EncryptUtils.getSecondKeyFromStr(str);
- fileMetaData.encryptType = propertiesMap.get("encryptType");
- } else if (propertiesMap.get("encryptLevel").equals("2")) {
+ fileMetaData.secondKey = EncryptUtils.getSecondKeyFromStr(encryptKey);
+ fileMetaData.encryptType = getPropertyAsString(propertiesMap,
"encryptType");
+ } else if (encryptLevel.equals("2")) {
if (!propertiesMap.containsKey("encryptType")) {
throw new EncryptException(
Messages.format("error.file.tsfile_metadata_no_encrypt_type",
2));
@@ -157,19 +164,17 @@ public class TsFileMetadata {
throw new EncryptException(
Messages.format("error.file.tsfile_metadata_no_encrypt_key", 2));
}
- if (propertiesMap.get("encryptKey") == null ||
propertiesMap.get("encryptKey").isEmpty()) {
+ String encryptKey = getPropertyAsString(propertiesMap, "encryptKey");
+ if (encryptKey == null || encryptKey.isEmpty()) {
throw new EncryptException(
Messages.format("error.file.tsfile_metadata_null_encrypt_key",
2));
}
fileMetaData.encryptLevel = 2;
- String str = propertiesMap.get("encryptKey");
- fileMetaData.secondKey = EncryptUtils.getSecondKeyFromStr(str);
- fileMetaData.encryptType = propertiesMap.get("encryptType");
+ fileMetaData.secondKey = EncryptUtils.getSecondKeyFromStr(encryptKey);
+ fileMetaData.encryptType = getPropertyAsString(propertiesMap,
"encryptType");
} else {
throw new EncryptException(
- Messages.format(
- "error.file.tsfile_metadata_unsupported_encrypt_level",
- propertiesMap.get("encryptLevel")));
+
Messages.format("error.file.tsfile_metadata_unsupported_encrypt_level",
encryptLevel));
}
fileMetaData.tsFileProperties = propertiesMap;
}
@@ -177,7 +182,7 @@ public class TsFileMetadata {
return fileMetaData;
}
- public void addProperty(String key, String value) {
+ public void addProperty(String key, byte[] value) {
if (tsFileProperties == null) {
tsFileProperties = new HashMap<>();
}
@@ -248,9 +253,15 @@ public class TsFileMetadata {
ReadWriteForEncodingUtils.writeVarInt(
tsFileProperties != null ? tsFileProperties.size() : 0,
outputStream);
if (tsFileProperties != null) {
- for (Entry<String, String> entry : tsFileProperties.entrySet()) {
+ for (Entry<String, byte[]> entry : tsFileProperties.entrySet()) {
byteLen += ReadWriteIOUtils.writeVar(entry.getKey(), outputStream);
- byteLen += ReadWriteIOUtils.writeVar(entry.getValue(), outputStream);
+ byte[] value = entry.getValue();
+ byteLen +=
+ ReadWriteForEncodingUtils.writeVarInt(value == null ? -1 :
value.length, outputStream);
+ if (value != null) {
+ outputStream.write(value);
+ byteLen += value.length;
+ }
}
}
@@ -295,7 +306,16 @@ public class TsFileMetadata {
return tableSchemaMap;
}
- public Map<String, String> getTsFileProperties() {
+ public Map<String, byte[]> getTsFileProperties() {
return tsFileProperties;
}
+
+ private static String getPropertyAsString(Map<String, byte[]> properties,
String key) {
+ byte[] value = properties.get(key);
+ return value == null ? null : new String(value,
TSFileConfig.STRING_CHARSET);
+ }
+
+ private static byte[] stringToBytes(String value) {
+ return value.getBytes(TSFileConfig.STRING_CHARSET);
+ }
}
diff --git
a/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileSequenceReader.java
b/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileSequenceReader.java
index 1fcabdab6..9407eda65 100644
--- a/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileSequenceReader.java
+++ b/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileSequenceReader.java
@@ -607,6 +607,11 @@ public class TsFileSequenceReader implements AutoCloseable
{
return getTableSchemaMap(null);
}
+ /** Get the properties stored in the TsFile metadata. */
+ public Map<String, byte[]> getTsFileProperties() throws IOException {
+ return readFileMetadata().getTsFileProperties();
+ }
+
public Map<String, TableSchema> getTableSchemaMap(LongConsumer
ioSizeRecorder)
throws IOException {
if (tsFileMetaData != null && tsFileMetaData.hasTableSchemaMapCache()) {
diff --git
a/java/tsfile/src/main/java/org/apache/tsfile/read/v4/DeviceTableModelReader.java
b/java/tsfile/src/main/java/org/apache/tsfile/read/v4/DeviceTableModelReader.java
index e7749d42b..a3d125a1d 100644
---
a/java/tsfile/src/main/java/org/apache/tsfile/read/v4/DeviceTableModelReader.java
+++
b/java/tsfile/src/main/java/org/apache/tsfile/read/v4/DeviceTableModelReader.java
@@ -78,6 +78,12 @@ public class DeviceTableModelReader implements ITsFileReader
{
return Optional.ofNullable(tableSchemaMap.get(tableName.toLowerCase()));
}
+ @Override
+ @TsFileApi
+ public Map<String, byte[]> getTsFileProperties() throws IOException {
+ return fileReader.getTsFileProperties();
+ }
+
@TsFileApi
public ResultSet query(String tableName, List<String> columnNames, long
startTime, long endTime)
throws IOException, NoTableException, NoMeasurementException,
ReadProcessException {
diff --git
a/java/tsfile/src/main/java/org/apache/tsfile/read/v4/ITsFileReader.java
b/java/tsfile/src/main/java/org/apache/tsfile/read/v4/ITsFileReader.java
index 995f73f64..dd0c188e1 100644
--- a/java/tsfile/src/main/java/org/apache/tsfile/read/v4/ITsFileReader.java
+++ b/java/tsfile/src/main/java/org/apache/tsfile/read/v4/ITsFileReader.java
@@ -29,6 +29,7 @@ import org.apache.tsfile.read.query.dataset.ResultSet;
import java.io.IOException;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
public interface ITsFileReader extends AutoCloseable {
@@ -48,6 +49,9 @@ public interface ITsFileReader extends AutoCloseable {
@TsFileApi
List<TableSchema> getAllTableSchema() throws IOException;
+ @TsFileApi
+ Map<String, byte[]> getTsFileProperties() throws IOException;
+
@TsFileApi
void close();
}
diff --git
a/java/tsfile/src/main/java/org/apache/tsfile/utils/TsFileTablePointCountTool.java
b/java/tsfile/src/main/java/org/apache/tsfile/utils/TsFileTablePointCountTool.java
new file mode 100644
index 000000000..973c0215e
--- /dev/null
+++
b/java/tsfile/src/main/java/org/apache/tsfile/utils/TsFileTablePointCountTool.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tsfile.utils;
+
+import org.apache.tsfile.common.conf.TSFileConfig;
+import org.apache.tsfile.enums.ColumnCategory;
+import org.apache.tsfile.file.metadata.IDeviceID;
+import org.apache.tsfile.file.metadata.TableSchema;
+import org.apache.tsfile.file.metadata.TimeseriesMetadata;
+import org.apache.tsfile.file.metadata.TsFileMetadata;
+import org.apache.tsfile.i18n.Messages;
+import org.apache.tsfile.read.TsFileSequenceReader;
+import org.apache.tsfile.write.schema.IMeasurementSchema;
+import org.apache.tsfile.write.writer.TsFileIOWriter;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/** Detects and backfills table-level point-count properties in a complete
TsFile. */
+public final class TsFileTablePointCountTool {
+
+ private TsFileTablePointCountTool() {}
+
+ public enum UpdateStatus {
+ UPDATED,
+ ALREADY_PRESENT,
+ NO_TABLE
+ }
+
+ public static void main(String[] args) throws IOException {
+ if (args.length != 1) {
+
System.err.println(Messages.get("error.utils.table_point_count_tool_usage"));
+ return;
+ }
+ File file = new File(args[0]);
+ UpdateStatus status = updateTablePointCountIfMissing(file);
+ System.out.println(
+ Messages.format(
+ "info.utils.table_point_count_tool_result",
file.getAbsolutePath(), status));
+ }
+
+ /**
+ * Returns whether every table in the file has a valid non-negative
point-count property.
+ * Tree-model-only files return {@code false}.
+ */
+ public static boolean containsTablePointCount(File file) throws IOException {
+ try (TsFileSequenceReader reader = openCompleteFile(file)) {
+ Map<String, TableSchema> tableSchemas = reader.getTableSchemaMap();
+ return !tableSchemas.isEmpty()
+ && hasAllTablePointCountProperties(reader.getTsFileProperties(),
tableSchemas.keySet());
+ }
+ }
+
+ /**
+ * Adds table point-count properties when they are missing. Existing
complete properties are left
+ * untouched. The source file is replaced only after a complete rewritten
copy has been forced to
+ * disk.
+ */
+ public static UpdateStatus updateTablePointCountIfMissing(File file) throws
IOException {
+ TsFileMetadata metadata;
+ long fileMetadataPosition;
+ Map<String, Long> tablePointCounts;
+ try (TsFileSequenceReader reader = openCompleteFile(file)) {
+ reader.setEnableCacheTableSchemaMap();
+ metadata = reader.readFileMetadata();
+ Map<String, TableSchema> tableSchemas = metadata.getTableSchemaMap();
+ if (tableSchemas.isEmpty()) {
+ return UpdateStatus.NO_TABLE;
+ }
+ if (hasAllTablePointCountProperties(metadata.getTsFileProperties(),
tableSchemas.keySet())) {
+ return UpdateStatus.ALREADY_PRESENT;
+ }
+ tablePointCounts = scanTablePointCounts(reader, tableSchemas);
+ fileMetadataPosition = reader.getFileMetadataPos();
+ }
+
+ tablePointCounts.forEach(
+ (tableName, pointCount) ->
+ metadata.addProperty(
+ TsFileIOWriter.TABLE_POINT_COUNT_PROPERTY_PREFIX + tableName,
+ BytesUtils.longToBytes(pointCount)));
+ rewriteFileMetadata(file.toPath(), fileMetadataPosition, metadata);
+ return UpdateStatus.UPDATED;
+ }
+
+ static Map<String, Long> scanTablePointCounts(
+ TsFileSequenceReader reader, Map<String, TableSchema> tableSchemas)
throws IOException {
+ Map<String, Set<String>> fieldNamesByTable = new HashMap<>();
+ Map<String, Long> tablePointCounts = new HashMap<>();
+ for (Map.Entry<String, TableSchema> tableEntry : tableSchemas.entrySet()) {
+ List<IMeasurementSchema> columns =
tableEntry.getValue().getColumnSchemas();
+ List<ColumnCategory> columnCategories =
tableEntry.getValue().getColumnTypes();
+ Set<String> fieldNames = new HashSet<>();
+ for (int i = 0; i < columns.size(); i++) {
+ if (columnCategories.get(i) == ColumnCategory.FIELD) {
+ fieldNames.add(columns.get(i).getMeasurementName());
+ }
+ }
+ fieldNamesByTable.put(tableEntry.getKey(), fieldNames);
+ tablePointCounts.put(tableEntry.getKey(), 0L);
+ }
+
+ Iterator<Pair<IDeviceID, List<TimeseriesMetadata>>> metadataIterator =
+ reader.iterAllTimeseriesMetadata(false, false);
+ while (metadataIterator.hasNext()) {
+ Pair<IDeviceID, List<TimeseriesMetadata>> deviceMetadata =
metadataIterator.next();
+ IDeviceID device = deviceMetadata.left;
+ String tableName = device.getTableName();
+ Set<String> fieldNames = fieldNamesByTable.getOrDefault(tableName,
Collections.emptySet());
+ if (fieldNames.isEmpty()) {
+ continue;
+ }
+ for (TimeseriesMetadata timeseriesMetadata : deviceMetadata.right) {
+ if (fieldNames.contains(timeseriesMetadata.getMeasurementId())) {
+ tablePointCounts.merge(
+ tableName, (long) timeseriesMetadata.getStatistics().getCount(),
Long::sum);
+ }
+ }
+ }
+ return tablePointCounts;
+ }
+
+ private static TsFileSequenceReader openCompleteFile(File file) throws
IOException {
+ if (!file.isFile()) {
+ throw new IOException(
+ Messages.format("error.utils.table_point_count_tool_file_not_found",
file));
+ }
+ TsFileSequenceReader reader = new
TsFileSequenceReader(file.getAbsolutePath());
+ try {
+ if (!reader.isComplete()) {
+ throw new IOException(
+
Messages.format("error.utils.table_point_count_tool_incomplete_file", file));
+ }
+ return reader;
+ } catch (IOException | RuntimeException e) {
+ try {
+ reader.close();
+ } catch (IOException closeException) {
+ e.addSuppressed(closeException);
+ }
+ throw e;
+ }
+ }
+
+ private static boolean hasAllTablePointCountProperties(
+ Map<String, byte[]> properties, Set<String> tableNames) {
+ if (properties == null) {
+ return false;
+ }
+ for (String tableName : tableNames) {
+ byte[] value =
properties.get(TsFileIOWriter.TABLE_POINT_COUNT_PROPERTY_PREFIX + tableName);
+ try {
+ if (value == null || value.length != Long.BYTES ||
BytesUtils.bytesToLong(value) < 0) {
+ return false;
+ }
+ } catch (RuntimeException e) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static void rewriteFileMetadata(
+ Path sourceFile, long fileMetadataPosition, TsFileMetadata metadata)
throws IOException {
+ Path absoluteSource = sourceFile.toAbsolutePath();
+ // Keep the temporary file beside the source so reflink and atomic move
can stay on the same
+ // file system. The source is not replaced until the rewritten temporary
file has been forced.
+ Path temporaryFile =
+ Files.createTempFile(
+ absoluteSource.getParent(),
+ absoluteSource.getFileName().toString(),
+ ".point-count.tmp");
+ try {
+ if (tryCreateReflink(absoluteSource, temporaryFile)) {
+ // A reflink shares unchanged data blocks with the source. Truncating
and rewriting only the
+ // TsFileMetadata tail therefore triggers copy-on-write for the
affected tail blocks.
+ try (FileChannel targetChannel =
+ FileChannel.open(temporaryFile, StandardOpenOption.WRITE)) {
+ targetChannel.truncate(fileMetadataPosition);
+ writeFileMetadata(targetChannel, fileMetadataPosition, metadata);
+ }
+ } else {
+ // Reflink is an optional optimization. Copy the immutable data/index
prefix when the
+ // command or file system does not support copy-on-write cloning.
+ try (FileChannel sourceChannel = FileChannel.open(absoluteSource,
StandardOpenOption.READ);
+ FileChannel targetChannel =
+ FileChannel.open(
+ temporaryFile,
+ StandardOpenOption.WRITE,
+ StandardOpenOption.TRUNCATE_EXISTING)) {
+ copyPrefix(sourceChannel, targetChannel, fileMetadataPosition);
+ writeFileMetadata(targetChannel, fileMetadataPosition, metadata);
+ }
+ }
+ replaceSourceFile(temporaryFile, absoluteSource);
+ } finally {
+ Files.deleteIfExists(temporaryFile);
+ }
+ }
+
+ /**
+ * Attempts a same-file-system copy-on-write clone using the native platform
command. GNU/Linux
+ * uses {@code cp --reflink=always}, while macOS uses {@code cp -c}.
Unsupported platforms,
+ * missing commands, unsupported file systems, invalid clone results, and
command failures return
+ * {@code false} so the caller can safely fall back to a full prefix copy.
+ */
+ private static boolean tryCreateReflink(Path sourceFile, Path targetFile)
throws IOException {
+ String operatingSystem = System.getProperty("os.name",
"").toLowerCase(Locale.ROOT);
+ List<String> command;
+ if (operatingSystem.contains("linux")) {
+ command =
+ List.of("cp", "--reflink=always", "--", sourceFile.toString(),
targetFile.toString());
+ } else if (operatingSystem.contains("mac")) {
+ command = List.of("cp", "-c", sourceFile.toString(),
targetFile.toString());
+ } else {
+ return false;
+ }
+
+ try {
+ Process process =
+ new ProcessBuilder(command)
+ .redirectErrorStream(true)
+ .redirectOutput(ProcessBuilder.Redirect.DISCARD)
+ .start();
+ // A reflink should complete quickly regardless of file size. Bound the
external command so
+ // an unavailable or unhealthy file system cannot block the repair
indefinitely.
+ if (!process.waitFor(30, TimeUnit.SECONDS)) {
+ process.destroyForcibly();
+ // Wait for termination before the fallback path opens and rewrites
the same temporary file.
+ process.waitFor();
+ return false;
+ }
+ // Do not trust the exit code alone: verify that the clone produced a
complete file before
+ // truncating its metadata tail.
+ return process.exitValue() == 0
+ && Files.isRegularFile(targetFile)
+ && Files.size(targetFile) == Files.size(sourceFile);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IOException(e);
+ } catch (IOException | SecurityException e) {
+ return false;
+ }
+ }
+
+ /** Writes the replacement TsFileMetadata, its serialized size, and the
trailing TsFile magic. */
+ private static void writeFileMetadata(
+ FileChannel targetChannel, long fileMetadataPosition, TsFileMetadata
metadata)
+ throws IOException {
+ targetChannel.position(fileMetadataPosition);
+ OutputStream output = Channels.newOutputStream(targetChannel);
+ int metadataSize = metadata.serializeTo(output);
+ ReadWriteIOUtils.write(metadataSize, output);
+
output.write(TSFileConfig.MAGIC_STRING.getBytes(TSFileConfig.STRING_CHARSET));
+ output.flush();
+ targetChannel.force(true);
+ }
+
+ /** Copies the immutable data and metadata-index prefix used by the
non-reflink fallback. */
+ private static void copyPrefix(
+ FileChannel sourceChannel, FileChannel targetChannel, long prefixLength)
throws IOException {
+ long copied = 0;
+ while (copied < prefixLength) {
+ long currentCopied = sourceChannel.transferTo(copied, prefixLength -
copied, targetChannel);
+ if (currentCopied <= 0) {
+ throw new
IOException(Messages.get("error.utils.table_point_count_tool_copy_failed"));
+ }
+ copied += currentCopied;
+ }
+ }
+
+ private static void replaceSourceFile(Path temporaryFile, Path sourceFile)
throws IOException {
+ try {
+ // Prefer an atomic replacement so readers never observe a partially
rewritten TsFile.
+ Files.move(
+ temporaryFile,
+ sourceFile,
+ StandardCopyOption.ATOMIC_MOVE,
+ StandardCopyOption.REPLACE_EXISTING);
+ } catch (AtomicMoveNotSupportedException e) {
+ // Some file systems do not provide atomic move even within one
directory.
+ Files.move(temporaryFile, sourceFile,
StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+}
diff --git
a/java/tsfile/src/main/java/org/apache/tsfile/write/v4/AbstractTableModelTsFileWriter.java
b/java/tsfile/src/main/java/org/apache/tsfile/write/v4/AbstractTableModelTsFileWriter.java
index 94be1fdc1..108de0051 100644
---
a/java/tsfile/src/main/java/org/apache/tsfile/write/v4/AbstractTableModelTsFileWriter.java
+++
b/java/tsfile/src/main/java/org/apache/tsfile/write/v4/AbstractTableModelTsFileWriter.java
@@ -259,6 +259,12 @@ abstract class AbstractTableModelTsFileWriter implements
ITsFileWriter {
return fileWriter.getSchema();
}
+ @Override
+ @TsFileApi
+ public void addTsFileProperty(String key, byte[] value) {
+ fileWriter.addTsFileProperty(key, value);
+ }
+
/**
* calling this method to write the last data remaining in memory and close
the normal and error
* OutputStream.
diff --git
a/java/tsfile/src/main/java/org/apache/tsfile/write/v4/ITsFileWriter.java
b/java/tsfile/src/main/java/org/apache/tsfile/write/v4/ITsFileWriter.java
index 25ccb031b..d20c432c5 100644
--- a/java/tsfile/src/main/java/org/apache/tsfile/write/v4/ITsFileWriter.java
+++ b/java/tsfile/src/main/java/org/apache/tsfile/write/v4/ITsFileWriter.java
@@ -36,4 +36,7 @@ public interface ITsFileWriter extends AutoCloseable {
@TsFileApi
void write(TSRecord record) throws IOException, WriteProcessException;
+
+ @TsFileApi
+ void addTsFileProperty(String key, byte[] value);
}
diff --git
a/java/tsfile/src/main/java/org/apache/tsfile/write/writer/TsFileIOWriter.java
b/java/tsfile/src/main/java/org/apache/tsfile/write/writer/TsFileIOWriter.java
index df6d99b20..93ddec569 100644
---
a/java/tsfile/src/main/java/org/apache/tsfile/write/writer/TsFileIOWriter.java
+++
b/java/tsfile/src/main/java/org/apache/tsfile/write/writer/TsFileIOWriter.java
@@ -126,6 +126,7 @@ public class TsFileIOWriter implements AutoCloseable {
protected LinkedList<Long> endPosInCMTForDevice = new LinkedList<>();
private volatile int chunkMetadataCount = 0;
public static final String CHUNK_METADATA_TEMP_FILE_SUFFIX = ".meta";
+ public static final String TABLE_POINT_COUNT_PROPERTY_PREFIX =
"tablePointCount.";
private boolean generateTableSchema = false;
@@ -143,6 +144,10 @@ public class TsFileIOWriter implements AutoCloseable {
protected Map<String, Long> tableSizeMap = new HashMap<>();
+ private final Map<String, byte[]> tsFileProperties = new HashMap<>();
+ private final Map<String, Long> tablePointCountMap = new HashMap<>();
+ private boolean recordTablePointCount;
+
/** empty construct function. */
protected TsFileIOWriter() {
setEncryptParam(
@@ -163,6 +168,15 @@ public class TsFileIOWriter implements AutoCloseable {
this(file, TS_FILE_CONFIG);
}
+ /**
+ * Creates a writer and optionally records the number of non-null field
points for each table in
+ * the TsFile properties.
+ */
+ public TsFileIOWriter(File file, boolean recordTablePointCount) throws
IOException {
+ this(file);
+ this.recordTablePointCount = recordTablePointCount;
+ }
+
public TsFileIOWriter(File file, EncryptParameter param) throws IOException {
this(file, TS_FILE_CONFIG, param);
}
@@ -246,6 +260,11 @@ public class TsFileIOWriter implements AutoCloseable {
}
}
+ /** Add a custom property to the TsFile metadata. */
+ public void addTsFileProperty(String key, byte[] value) {
+ tsFileProperties.put(key, value);
+ }
+
public void addFlushListener(FlushChunkMetadataListener listener) {
flushListeners.add(listener);
}
@@ -361,6 +380,7 @@ public class TsFileIOWriter implements AutoCloseable {
chunkHeader.getCompressionType(),
out.getPosition(),
chunkMetadata.getStatistics());
+ currentChunkMetadata.setMask(chunkMetadata.getMask());
chunkHeader.serializeTo(out.wrapAsStream());
out.write(chunk.getData());
endCurrentChunk();
@@ -412,6 +432,10 @@ public class TsFileIOWriter implements AutoCloseable {
chunkHeader.getCompressionType(),
out.getPosition(),
chunk.getChunkStatistic());
+ currentChunkMetadata.setMask(
+ (byte)
+ (chunkHeader.getChunkType()
+ & (TsFileConstant.TIME_COLUMN_MASK |
TsFileConstant.VALUE_COLUMN_MASK)));
chunkHeader.serializeTo(out.wrapAsStream());
out.write(chunk.getData());
endCurrentChunk();
@@ -421,6 +445,15 @@ public class TsFileIOWriter implements AutoCloseable {
public void endCurrentChunk() {
this.currentChunkMetadataSize +=
currentChunkMetadata.getRetainedSizeInBytes();
chunkMetadataCount++;
+ if (recordTablePointCount
+ && currentChunkGroupDeviceId != null
+ && currentChunkGroupDeviceId.isTableModel()
+ && (currentChunkMetadata.getMask() & TsFileConstant.TIME_COLUMN_MASK)
== 0) {
+ tablePointCountMap.merge(
+ currentChunkGroupDeviceId.getTableName(),
+ currentChunkMetadata.getNumOfPoints(),
+ Long::sum);
+ }
chunkMetadataList.add(currentChunkMetadata);
currentChunkMetadata = null;
}
@@ -590,9 +623,18 @@ public class TsFileIOWriter implements AutoCloseable {
tsFileMetadata.setTableSchemaMap(schema.getTableSchemaMap());
tsFileMetadata.setMetaOffset(metaOffset);
tsFileMetadata.setBloomFilter(filter);
- tsFileMetadata.addProperty("encryptLevel", encryptLevel);
- tsFileMetadata.addProperty("encryptType", encryptType);
- tsFileMetadata.addProperty("encryptKey", encryptKey);
+ tsFileProperties.forEach(tsFileMetadata::addProperty);
+ if (recordTablePointCount) {
+ tablePointCountMap.forEach(
+ (tableName, pointCount) ->
+ tsFileMetadata.addProperty(
+ TABLE_POINT_COUNT_PROPERTY_PREFIX + tableName,
+ BytesUtils.longToBytes(pointCount)));
+ }
+ tsFileMetadata.addProperty("encryptLevel",
encryptLevel.getBytes(TSFileConfig.STRING_CHARSET));
+ tsFileMetadata.addProperty("encryptType",
encryptType.getBytes(TSFileConfig.STRING_CHARSET));
+ tsFileMetadata.addProperty(
+ "encryptKey", encryptKey == null ? null :
encryptKey.getBytes(TSFileConfig.STRING_CHARSET));
int size = tsFileMetadata.serializeTo(out.wrapAsStream());
diff --git
a/java/tsfile/src/test/java/org/apache/tsfile/file/metadata/TsFileMetadataTest.java
b/java/tsfile/src/test/java/org/apache/tsfile/file/metadata/TsFileMetadataTest.java
index dcf343918..6488acde9 100644
---
a/java/tsfile/src/test/java/org/apache/tsfile/file/metadata/TsFileMetadataTest.java
+++
b/java/tsfile/src/test/java/org/apache/tsfile/file/metadata/TsFileMetadataTest.java
@@ -18,6 +18,7 @@
*/
package org.apache.tsfile.file.metadata;
+import org.apache.tsfile.common.conf.TSFileConfig;
import org.apache.tsfile.compatibility.DeserializeConfig;
import org.apache.tsfile.constant.TestConstant;
import org.apache.tsfile.file.metadata.utils.TestHelper;
@@ -53,10 +54,12 @@ public class TsFileMetadataTest {
@Test
public void testWriteFileMetaData() {
TsFileMetadata tsfMetaData = TestHelper.createSimpleFileMetaData();
- tsfMetaData.addProperty("encryptLevel", "0");
- tsfMetaData.addProperty("encryptType",
"org.apache.tsfile.encrypt.UNENCRYPTED");
- tsfMetaData.addProperty("encryptKey", "");
- tsfMetaData.addProperty("d", "1");
+ tsfMetaData.addProperty("encryptLevel",
"0".getBytes(TSFileConfig.STRING_CHARSET));
+ tsfMetaData.addProperty(
+ "encryptType",
+
"org.apache.tsfile.encrypt.UNENCRYPTED".getBytes(TSFileConfig.STRING_CHARSET));
+ tsfMetaData.addProperty("encryptKey",
"".getBytes(TSFileConfig.STRING_CHARSET));
+ tsfMetaData.addProperty("d", new byte[] {0, 1, -1});
serialized(tsfMetaData);
TsFileMetadata readMetaData = deSerialized();
Assert.assertTrue(Utils.isFileMetaDataEqual(tsfMetaData, readMetaData));
diff --git
a/java/tsfile/src/test/java/org/apache/tsfile/file/metadata/utils/Utils.java
b/java/tsfile/src/test/java/org/apache/tsfile/file/metadata/utils/Utils.java
index dc37e00f9..27ec1ab24 100644
--- a/java/tsfile/src/test/java/org/apache/tsfile/file/metadata/utils/Utils.java
+++ b/java/tsfile/src/test/java/org/apache/tsfile/file/metadata/utils/Utils.java
@@ -25,7 +25,8 @@ import
org.apache.tsfile.file.metadata.statistics.BooleanStatistics;
import org.apache.tsfile.file.metadata.statistics.IntegerStatistics;
import org.apache.tsfile.file.metadata.statistics.Statistics;
-import java.util.Objects;
+import java.util.Arrays;
+import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@@ -61,13 +62,29 @@ public class Utils {
MetadataIndexNode metaDataIndex2 =
metadata2.getTableMetadataIndexNode(TestHelper.TEST_TABLE_NAME);
- return Objects.equals(metadata1.getTsFileProperties(),
metadata2.getTsFileProperties())
+ return arePropertiesEqual(metadata1.getTsFileProperties(),
metadata2.getTsFileProperties())
&& metaDataIndex1.getChildren().size() ==
metaDataIndex2.getChildren().size();
}
}
return false;
}
+ private static boolean arePropertiesEqual(
+ Map<String, byte[]> properties1, Map<String, byte[]> properties2) {
+ if (properties1 == null || properties2 == null) {
+ return properties1 == properties2;
+ }
+ if (!properties1.keySet().equals(properties2.keySet())) {
+ return false;
+ }
+ for (String key : properties1.keySet()) {
+ if (!Arrays.equals(properties1.get(key), properties2.get(key))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
public static void isPageHeaderEqual(PageHeader header1, PageHeader header2)
{
if (Utils.isTwoObjectsNotNULL(header1, header2, "PageHeader")) {
assertEquals(header1.getUncompressedSize(),
header2.getUncompressedSize());
diff --git
a/java/tsfile/src/test/java/org/apache/tsfile/read/TsFileV4ReadWriteInterfacesTest.java
b/java/tsfile/src/test/java/org/apache/tsfile/read/TsFileV4ReadWriteInterfacesTest.java
index 51aa64ce6..bbcc3278b 100644
---
a/java/tsfile/src/test/java/org/apache/tsfile/read/TsFileV4ReadWriteInterfacesTest.java
+++
b/java/tsfile/src/test/java/org/apache/tsfile/read/TsFileV4ReadWriteInterfacesTest.java
@@ -27,6 +27,7 @@ import org.apache.tsfile.file.metadata.IDeviceID;
import org.apache.tsfile.file.metadata.StringArrayDeviceID;
import org.apache.tsfile.file.metadata.TableSchema;
import org.apache.tsfile.read.v4.DeviceTableModelReader;
+import org.apache.tsfile.read.v4.ITsFileReader;
import org.apache.tsfile.utils.Pair;
import org.apache.tsfile.utils.TsFileGeneratorForTest;
import org.apache.tsfile.utils.TsFileGeneratorUtils;
@@ -41,6 +42,7 @@ import org.junit.Test;
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -49,6 +51,43 @@ import java.util.List;
public class TsFileV4ReadWriteInterfacesTest {
+ @Test
+ public void testTsFileProperties() throws IOException {
+ String filePath = TsFileGeneratorForTest.getTestTsFilePath("properties",
0, 0, 0);
+ File file = new File(filePath);
+ TableSchema tableSchema =
+ new TableSchema(
+ "t1",
+ Arrays.asList(new MeasurementSchema("s1", TSDataType.INT32)),
+ Arrays.asList(ColumnCategory.FIELD));
+
+ try {
+ try (ITsFileWriter writer =
+ new
TsFileWriterBuilder().file(file).tableSchema(tableSchema).build()) {
+ writer.addTsFileProperty(
+ "creator",
"TsFileV4ReadWriteInterfacesTest".getBytes(StandardCharsets.UTF_8));
+ writer.addTsFileProperty("version",
"1".getBytes(StandardCharsets.UTF_8));
+ }
+
+ try (TsFileSequenceReader reader = new TsFileSequenceReader(filePath)) {
+ Assert.assertArrayEquals(
+ "TsFileV4ReadWriteInterfacesTest".getBytes(StandardCharsets.UTF_8),
+ reader.getTsFileProperties().get("creator"));
+ Assert.assertArrayEquals(
+ "1".getBytes(StandardCharsets.UTF_8),
reader.getTsFileProperties().get("version"));
+ }
+ try (ITsFileReader reader = new DeviceTableModelReader(file)) {
+ Assert.assertArrayEquals(
+ "TsFileV4ReadWriteInterfacesTest".getBytes(StandardCharsets.UTF_8),
+ reader.getTsFileProperties().get("creator"));
+ Assert.assertArrayEquals(
+ "1".getBytes(StandardCharsets.UTF_8),
reader.getTsFileProperties().get("version"));
+ }
+ } finally {
+ Files.deleteIfExists(file.toPath());
+ }
+ }
+
@Test
public void testWriteSomeColumns() throws IOException, WriteProcessException
{
String filePath = TsFileGeneratorForTest.getTestTsFilePath("db", 0, 0, 0);
diff --git
a/java/tsfile/src/test/java/org/apache/tsfile/utils/TsFileTablePointCountToolTest.java
b/java/tsfile/src/test/java/org/apache/tsfile/utils/TsFileTablePointCountToolTest.java
new file mode 100644
index 000000000..4133f031b
--- /dev/null
+++
b/java/tsfile/src/test/java/org/apache/tsfile/utils/TsFileTablePointCountToolTest.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tsfile.utils;
+
+import org.apache.tsfile.enums.ColumnCategory;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.metadata.ChunkMetadata;
+import org.apache.tsfile.file.metadata.ColumnSchema;
+import org.apache.tsfile.file.metadata.IDeviceID;
+import org.apache.tsfile.file.metadata.TableSchema;
+import org.apache.tsfile.read.TsFileSequenceReader;
+import org.apache.tsfile.write.TsFileWriter;
+import org.apache.tsfile.write.record.Tablet;
+import org.apache.tsfile.write.schema.IMeasurementSchema;
+import org.apache.tsfile.write.writer.TsFileIOWriter;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+public class TsFileTablePointCountToolTest {
+
+ /**
+ * Creates a two-table file without point-count properties. The repair tool
must traverse the
+ * metadata index tree and count only non-null FIELD values, preserve
unrelated properties and
+ * file data, and leave the file byte-for-byte unchanged when run again.
+ */
+ @Test
+ public void backfillMissingTablePointCountProperties() throws Exception {
+ File file = File.createTempFile("table-point-count-tool", ".tsfile");
+ try {
+ TableSchema tableSchema1 =
+ new TableSchema(
+ "table1",
+ Arrays.asList(
+ new ColumnSchema("device", TSDataType.STRING,
ColumnCategory.TAG),
+ new ColumnSchema("s1", TSDataType.INT32,
ColumnCategory.FIELD),
+ new ColumnSchema("s2", TSDataType.INT32,
ColumnCategory.FIELD)));
+ TableSchema tableSchema2 =
+ new TableSchema(
+ "table2",
+ Arrays.asList(
+ new ColumnSchema("device", TSDataType.STRING,
ColumnCategory.TAG),
+ new ColumnSchema("s1", TSDataType.INT32,
ColumnCategory.FIELD)));
+ try (TsFileIOWriter ioWriter = new TsFileIOWriter(file);
+ TsFileWriter writer = new TsFileWriter(ioWriter)) {
+ ioWriter.addTsFileProperty("custom.property",
"preserved".getBytes(StandardCharsets.UTF_8));
+ writer.registerTableSchema(tableSchema1);
+ writer.registerTableSchema(tableSchema2);
+ writer.writeTable(createTablet(tableSchema1, 3, true));
+ writer.writeTable(createTablet(tableSchema2, 2, false));
+ }
+
+ try (TsFileSequenceReader reader =
+ new TsFileSequenceReader(file.getAbsolutePath()) {
+ @Override
+ public List<IDeviceID> getAllDevices() {
+ throw new AssertionError("The tool must not enumerate all
devices");
+ }
+
+ @Override
+ public Map<String, List<ChunkMetadata>>
readChunkMetadataInDevice(IDeviceID device) {
+ throw new AssertionError("The tool must not query chunk metadata
by device");
+ }
+ }) {
+ Map<String, Long> pointCounts =
+ TsFileTablePointCountTool.scanTablePointCounts(reader,
reader.getTableSchemaMap());
+ Assert.assertEquals(Long.valueOf(5), pointCounts.get("table1"));
+ Assert.assertEquals(Long.valueOf(2), pointCounts.get("table2"));
+ }
+
+
Assert.assertFalse(TsFileTablePointCountTool.containsTablePointCount(file));
+ Assert.assertEquals(
+ TsFileTablePointCountTool.UpdateStatus.UPDATED,
+ TsFileTablePointCountTool.updateTablePointCountIfMissing(file));
+
Assert.assertTrue(TsFileTablePointCountTool.containsTablePointCount(file));
+
+ try (TsFileSequenceReader reader = new
TsFileSequenceReader(file.getAbsolutePath())) {
+ Assert.assertTrue(reader.isComplete());
+ Map<String, byte[]> properties = reader.getTsFileProperties();
+ Assert.assertArrayEquals(
+ "preserved".getBytes(StandardCharsets.UTF_8),
properties.get("custom.property"));
+ Assert.assertArrayEquals(
+ BytesUtils.longToBytes(5),
+ properties.get(TsFileIOWriter.TABLE_POINT_COUNT_PROPERTY_PREFIX +
"table1"));
+ Assert.assertArrayEquals(
+ BytesUtils.longToBytes(2),
+ properties.get(TsFileIOWriter.TABLE_POINT_COUNT_PROPERTY_PREFIX +
"table2"));
+ Assert.assertEquals(2, reader.getAllDevices().size());
+ }
+
+ byte[] repairedFile = Files.readAllBytes(file.toPath());
+ Assert.assertEquals(
+ TsFileTablePointCountTool.UpdateStatus.ALREADY_PRESENT,
+ TsFileTablePointCountTool.updateTablePointCountIfMissing(file));
+ Assert.assertArrayEquals(repairedFile,
Files.readAllBytes(file.toPath()));
+ } finally {
+ Files.deleteIfExists(file.toPath());
+ }
+ }
+
+ private Tablet createTablet(TableSchema tableSchema, int rowCount, boolean
nullableSecondField) {
+ Tablet tablet =
+ new Tablet(
+ tableSchema.getTableName(),
+
IMeasurementSchema.getMeasurementNameList(tableSchema.getColumnSchemas()),
+ IMeasurementSchema.getDataTypeList(tableSchema.getColumnSchemas()),
+ tableSchema.getColumnTypes());
+ for (int row = 0; row < rowCount; row++) {
+ tablet.addTimestamp(row, row);
+ tablet.addValue("device", row, "d1");
+ tablet.addValue("s1", row, row);
+ if (nullableSecondField) {
+ tablet.addValue("s2", row, row == rowCount - 1 ? null : row);
+ }
+ }
+ return tablet;
+ }
+}
diff --git
a/java/tsfile/src/test/java/org/apache/tsfile/write/TablePointCountPerformanceTest.java
b/java/tsfile/src/test/java/org/apache/tsfile/write/TablePointCountPerformanceTest.java
new file mode 100644
index 000000000..786b36e13
--- /dev/null
+++
b/java/tsfile/src/test/java/org/apache/tsfile/write/TablePointCountPerformanceTest.java
@@ -0,0 +1,269 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tsfile.write;
+
+import org.apache.tsfile.enums.ColumnCategory;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.exception.write.WriteProcessException;
+import org.apache.tsfile.file.metadata.ColumnSchema;
+import org.apache.tsfile.file.metadata.TableSchema;
+import org.apache.tsfile.read.TsFileSequenceReader;
+import org.apache.tsfile.utils.BytesUtils;
+import org.apache.tsfile.write.record.Tablet;
+import org.apache.tsfile.write.schema.IMeasurementSchema;
+import org.apache.tsfile.write.writer.TsFileIOWriter;
+
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+public class TablePointCountPerformanceTest {
+
+ private static final String MODE_PROPERTY = "tablePointCountEnabled";
+ private static final int TABLE1_ROW_COUNT = 200_000;
+ private static final int TABLE2_ROW_COUNT = 100_000;
+ private static final int BATCH_SIZE = 100;
+ private static final long TABLE1_POINT_COUNT = 333_333;
+ private static final long TABLE2_POINT_COUNT = 100_000;
+
+ /**
+ * Measures one table point-count mode per JVM. Invoke this test twice with
{@code
+ * -DtablePointCountEnabled=false} and {@code true}; each JVM performs its
own warm-up and writes
+ * a mode-specific metrics file. Once both files exist, the test generates a
comparison report
+ * containing the two JVM process IDs, file sizes, and measured write
durations.
+ */
+ @Test
+ public void measureTablePointCountInDedicatedJvm() throws IOException,
WriteProcessException {
+ String configuredMode = System.getProperty(MODE_PROPERTY);
+ Assume.assumeTrue(
+ "Performance test requires -D" + MODE_PROPERTY + "=true|false",
configuredMode != null);
+ Assert.assertTrue(
+ "Invalid -D" + MODE_PROPERTY + " value: " + configuredMode,
+ "true".equals(configuredMode) || "false".equals(configuredMode));
+ boolean enabled = Boolean.parseBoolean(configuredMode);
+
+ TableSchema tableSchema1 =
+ new TableSchema(
+ "table1",
+ Arrays.asList(
+ new ColumnSchema("device", TSDataType.STRING,
ColumnCategory.TAG),
+ new ColumnSchema("s1", TSDataType.INT32, ColumnCategory.FIELD),
+ new ColumnSchema("s2", TSDataType.INT32,
ColumnCategory.FIELD)));
+ TableSchema tableSchema2 =
+ new TableSchema(
+ "table2",
+ Arrays.asList(
+ new ColumnSchema("device", TSDataType.STRING,
ColumnCategory.TAG),
+ new ColumnSchema("s1", TSDataType.INT32,
ColumnCategory.FIELD)));
+ List<Tablet> table1Tablets = createTablets(tableSchema1, TABLE1_ROW_COUNT,
true);
+ List<Tablet> table2Tablets = createTablets(tableSchema2, TABLE2_ROW_COUNT,
false);
+
+ File targetDirectory = new File("target");
+ File warmUpFile = new File(targetDirectory, "table-point-count-warm-up-" +
enabled + ".tsfile");
+ File measuredFile = new File(targetDirectory, "table-point-count-" +
enabled + ".tsfile");
+ try {
+ writeFile(warmUpFile, enabled, tableSchema1, tableSchema2,
table1Tablets, table2Tablets);
+ Assert.assertTrue(warmUpFile.delete());
+
+ long startNanos = System.nanoTime();
+ writeFile(measuredFile, enabled, tableSchema1, tableSchema2,
table1Tablets, table2Tablets);
+ long elapsedNanos = System.nanoTime() - startNanos;
+ Assert.assertTrue(elapsedNanos > 0);
+
+ verifyProperties(measuredFile, enabled);
+ long pid = ProcessHandle.current().pid();
+ writeMetrics(targetDirectory, enabled, pid, measuredFile.length(),
elapsedNanos);
+ generateReportIfBothModesExist(targetDirectory);
+ System.out.printf(
+ "Table point count enabled=%s: pid=%d, size=%d bytes, write=%d ns
(%.3f ms)%n",
+ enabled, pid, measuredFile.length(), elapsedNanos, elapsedNanos /
1_000_000.0);
+ } finally {
+ warmUpFile.delete();
+ measuredFile.delete();
+ }
+ }
+
+ private void writeFile(
+ File file,
+ boolean enabled,
+ TableSchema tableSchema1,
+ TableSchema tableSchema2,
+ List<Tablet> table1Tablets,
+ List<Tablet> table2Tablets)
+ throws IOException, WriteProcessException {
+ try (TsFileWriter writer = new TsFileWriter(new TsFileIOWriter(file,
enabled))) {
+ writer.registerTableSchema(tableSchema1);
+ writer.registerTableSchema(tableSchema2);
+ for (Tablet tablet : table1Tablets) {
+ writer.writeTable(tablet);
+ }
+ for (Tablet tablet : table2Tablets) {
+ writer.writeTable(tablet);
+ }
+ }
+ }
+
+ private List<Tablet> createTablets(
+ TableSchema tableSchema, int rowCount, boolean nullableSecondField) {
+ List<String> columnNames =
+
IMeasurementSchema.getMeasurementNameList(tableSchema.getColumnSchemas());
+ List<TSDataType> dataTypes =
IMeasurementSchema.getDataTypeList(tableSchema.getColumnSchemas());
+ List<Tablet> tablets = new ArrayList<>((rowCount + BATCH_SIZE - 1) /
BATCH_SIZE);
+ for (int startRow = 0; startRow < rowCount; startRow += BATCH_SIZE) {
+ int currentBatchSize = Math.min(BATCH_SIZE, rowCount - startRow);
+ Tablet tablet =
+ new Tablet(
+ tableSchema.getTableName(),
+ columnNames,
+ dataTypes,
+ tableSchema.getColumnTypes(),
+ currentBatchSize);
+ for (int row = 0; row < currentBatchSize; row++) {
+ int globalRow = startRow + row;
+ tablet.addTimestamp(row, globalRow);
+ tablet.addValue("device", row, "d1");
+ tablet.addValue("s1", row, globalRow);
+ if (nullableSecondField) {
+ tablet.addValue("s2", row, globalRow % 3 == 0 ? null : globalRow);
+ }
+ }
+ tablets.add(tablet);
+ }
+ return tablets;
+ }
+
+ private void verifyProperties(File file, boolean enabled) throws IOException
{
+ try (TsFileSequenceReader reader = new
TsFileSequenceReader(file.getAbsolutePath())) {
+ Map<String, byte[]> properties = reader.getTsFileProperties();
+ String table1Key = TsFileIOWriter.TABLE_POINT_COUNT_PROPERTY_PREFIX +
"table1";
+ String table2Key = TsFileIOWriter.TABLE_POINT_COUNT_PROPERTY_PREFIX +
"table2";
+ if (enabled) {
+ Assert.assertArrayEquals(
+ BytesUtils.longToBytes(TABLE1_POINT_COUNT),
properties.get(table1Key));
+ Assert.assertArrayEquals(
+ BytesUtils.longToBytes(TABLE2_POINT_COUNT),
properties.get(table2Key));
+ } else {
+ Assert.assertFalse(properties.containsKey(table1Key));
+ Assert.assertFalse(properties.containsKey(table2Key));
+ }
+ }
+ }
+
+ private void writeMetrics(
+ File targetDirectory, boolean enabled, long pid, long fileSize, long
elapsedNanos)
+ throws IOException {
+ Properties metrics = new Properties();
+ metrics.setProperty("enabled", Boolean.toString(enabled));
+ metrics.setProperty("pid", Long.toString(pid));
+ metrics.setProperty("fileSize", Long.toString(fileSize));
+ metrics.setProperty("elapsedNanos", Long.toString(elapsedNanos));
+ metrics.setProperty("rows", Integer.toString(TABLE1_ROW_COUNT +
TABLE2_ROW_COUNT));
+ metrics.setProperty("points", Long.toString(TABLE1_POINT_COUNT +
TABLE2_POINT_COUNT));
+ try (FileOutputStream output = new
FileOutputStream(metricsFile(targetDirectory, enabled))) {
+ metrics.store(output, "Table point count performance metrics");
+ }
+ }
+
+ private void generateReportIfBothModesExist(File targetDirectory) throws
IOException {
+ File disabledMetricsFile = metricsFile(targetDirectory, false);
+ File enabledMetricsFile = metricsFile(targetDirectory, true);
+ if (!disabledMetricsFile.isFile() || !enabledMetricsFile.isFile()) {
+ return;
+ }
+ Properties disabled = loadMetrics(disabledMetricsFile);
+ Properties enabled = loadMetrics(enabledMetricsFile);
+ long disabledPid = Long.parseLong(disabled.getProperty("pid"));
+ long enabledPid = Long.parseLong(enabled.getProperty("pid"));
+ Assert.assertNotEquals("Modes must run in separate JVMs", disabledPid,
enabledPid);
+ long disabledSize = Long.parseLong(disabled.getProperty("fileSize"));
+ long enabledSize = Long.parseLong(enabled.getProperty("fileSize"));
+ long disabledNanos = Long.parseLong(disabled.getProperty("elapsedNanos"));
+ long enabledNanos = Long.parseLong(enabled.getProperty("elapsedNanos"));
+ Assert.assertTrue(enabledSize > disabledSize);
+
+ String report =
+ String.format(
+ "# Table Point Count Performance Report%n%n"
+ + "## Dataset%n%n"
+ + "- table1: %,d rows, %,d non-null FIELD points%n"
+ + "- table2: %,d rows, %,d non-null FIELD points%n"
+ + "- Batch size: %,d rows%n"
+ + "- Total: %,d rows, %,d non-null FIELD points%n%n"
+ + "## Results%n%n"
+ + "| Point counting | JVM PID | File size (bytes) | Write time
(ns) | Write time (ms) |%n"
+ + "|---|---:|---:|---:|---:|%n"
+ + "| Disabled | %d | %,d | %,d | %.3f |%n"
+ + "| Enabled | %d | %,d | %,d | %.3f |%n%n"
+ + "- File-size delta: %,d bytes%n"
+ + "- File-size overhead: %.3f%%%n"
+ + "- Write-time delta: %,d ns (%.3f ms)%n"
+ + "- Write-time overhead: %.3f%%%n%n"
+ + "> Each mode ran in a separate JVM and performed an
independent warm-up. Write "
+ + "time includes writer construction, schema registration,
batched writes, "
+ + "metadata serialization, force, and close. Timing is
diagnostic and is not used "
+ + "as a CI pass/fail threshold.%n",
+ TABLE1_ROW_COUNT,
+ TABLE1_POINT_COUNT,
+ TABLE2_ROW_COUNT,
+ TABLE2_POINT_COUNT,
+ BATCH_SIZE,
+ TABLE1_ROW_COUNT + TABLE2_ROW_COUNT,
+ TABLE1_POINT_COUNT + TABLE2_POINT_COUNT,
+ disabledPid,
+ disabledSize,
+ disabledNanos,
+ disabledNanos / 1_000_000.0,
+ enabledPid,
+ enabledSize,
+ enabledNanos,
+ enabledNanos / 1_000_000.0,
+ enabledSize - disabledSize,
+ (enabledSize - disabledSize) * 100.0 / disabledSize,
+ enabledNanos - disabledNanos,
+ (enabledNanos - disabledNanos) / 1_000_000.0,
+ (enabledNanos - disabledNanos) * 100.0 / disabledNanos);
+ File reportFile = new File(targetDirectory,
"table-point-count-performance-report.md");
+ Files.write(reportFile.toPath(), report.getBytes(StandardCharsets.UTF_8));
+ System.out.printf("Table point-count report: %s%n",
reportFile.getAbsolutePath());
+ }
+
+ private Properties loadMetrics(File file) throws IOException {
+ Properties metrics = new Properties();
+ try (FileInputStream input = new FileInputStream(file)) {
+ metrics.load(input);
+ }
+ return metrics;
+ }
+
+ private File metricsFile(File targetDirectory, boolean enabled) {
+ return new File(targetDirectory, "table-point-count-" + enabled +
".properties");
+ }
+}
diff --git
a/java/tsfile/src/test/java/org/apache/tsfile/write/TsFileWriteApiTest.java
b/java/tsfile/src/test/java/org/apache/tsfile/write/TsFileWriteApiTest.java
index fcd761d3f..4341da538 100644
--- a/java/tsfile/src/test/java/org/apache/tsfile/write/TsFileWriteApiTest.java
+++ b/java/tsfile/src/test/java/org/apache/tsfile/write/TsFileWriteApiTest.java
@@ -44,6 +44,7 @@ import org.apache.tsfile.read.query.dataset.QueryDataSet;
import org.apache.tsfile.read.query.dataset.ResultSet;
import org.apache.tsfile.read.v4.ITsFileReader;
import org.apache.tsfile.read.v4.TsFileReaderBuilder;
+import org.apache.tsfile.utils.BytesUtils;
import org.apache.tsfile.utils.Pair;
import org.apache.tsfile.utils.TsFileGeneratorUtils;
import org.apache.tsfile.write.chunk.AlignedChunkWriterImpl;
@@ -1237,6 +1238,69 @@ public class TsFileWriteApiTest {
}
}
+ /**
+ * Verifies that table point counting records only non-null FIELD values in
TsFile properties.
+ * Table1 contains three s1 values and two non-null s2 values, while table2
contains two s1
+ * values. Timestamps, TAG values, and the null s2 value are excluded, so
the expected counts are
+ * five and two respectively.
+ */
+ @Test
+ public void recordTablePointCountInProperties() throws IOException,
WriteProcessException {
+ TableSchema tableSchema1 =
+ new TableSchema(
+ "table1",
+ Arrays.asList(
+ new ColumnSchema("device", TSDataType.STRING,
ColumnCategory.TAG),
+ new ColumnSchema("s1", TSDataType.INT32, ColumnCategory.FIELD),
+ new ColumnSchema("s2", TSDataType.INT32,
ColumnCategory.FIELD)));
+ TableSchema tableSchema2 =
+ new TableSchema(
+ "table2",
+ Arrays.asList(
+ new ColumnSchema("device", TSDataType.STRING,
ColumnCategory.TAG),
+ new ColumnSchema("s1", TSDataType.INT32,
ColumnCategory.FIELD)));
+ Tablet tablet1 =
+ new Tablet(
+ "table1",
+
IMeasurementSchema.getMeasurementNameList(tableSchema1.getColumnSchemas()),
+
IMeasurementSchema.getDataTypeList(tableSchema1.getColumnSchemas()),
+ tableSchema1.getColumnTypes());
+ for (int row = 0; row < 3; row++) {
+ tablet1.addTimestamp(row, row);
+ tablet1.addValue("device", row, "d1");
+ tablet1.addValue("s1", row, row);
+ tablet1.addValue("s2", row, row == 2 ? null : row);
+ }
+ Tablet tablet2 =
+ new Tablet(
+ "table2",
+
IMeasurementSchema.getMeasurementNameList(tableSchema2.getColumnSchemas()),
+
IMeasurementSchema.getDataTypeList(tableSchema2.getColumnSchemas()),
+ tableSchema2.getColumnTypes());
+ for (int row = 0; row < 2; row++) {
+ tablet2.addTimestamp(row, row);
+ tablet2.addValue("device", row, "d1");
+ tablet2.addValue("s1", row, row);
+ }
+
+ try (TsFileWriter writer = new TsFileWriter(new TsFileIOWriter(f, true))) {
+ writer.registerTableSchema(tableSchema1);
+ writer.registerTableSchema(tableSchema2);
+ writer.writeTable(tablet1);
+ writer.writeTable(tablet2);
+ }
+
+ try (TsFileSequenceReader reader = new
TsFileSequenceReader(f.getAbsolutePath())) {
+ Map<String, byte[]> properties = reader.getTsFileProperties();
+ Assert.assertArrayEquals(
+ BytesUtils.longToBytes(5),
+ properties.get(TsFileIOWriter.TABLE_POINT_COUNT_PROPERTY_PREFIX +
"table1"));
+ Assert.assertArrayEquals(
+ BytesUtils.longToBytes(2),
+ properties.get(TsFileIOWriter.TABLE_POINT_COUNT_PROPERTY_PREFIX +
"table2"));
+ }
+ }
+
@Test
public void calculateTableSize() throws IOException, WriteProcessException {
TableSchema tableSchema1 =