blob: 6965114561750377a90b6b913df397359e6bfe4e [file]
/*
* 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.
*/
#include "format/output_format.h"
#include <algorithm>
#include <cstdio>
#include <set>
#include <utility>
#include "common/csv_utils.h"
#include "utils/errno_define.h"
namespace tsfile_cli {
const char* error_code_message(int code) {
switch (code) {
case common::E_OOM:
return "out of memory";
case common::E_NOT_EXIST:
return "not found";
case common::E_INVALID_ARG:
return "invalid argument";
case common::E_OUT_OF_RANGE:
return "value out of range";
case common::E_OUT_OF_ORDER:
return "data is out of order";
case common::E_FILE_OPEN_ERR:
return "cannot open file";
case common::E_FILE_WRITE_ERR:
return "file write error";
case common::E_FILE_READ_ERR:
return "file read error";
case common::E_TSFILE_CORRUPTED:
return "file is corrupted";
case common::E_INVALID_PATH:
return "invalid path";
case common::E_DEVICE_NOT_EXIST:
return "device does not exist";
case common::E_MEASUREMENT_NOT_EXIST:
return "measurement does not exist";
case common::E_TABLE_NOT_EXIST:
return "table does not exist";
case common::E_COLUMN_NOT_EXIST:
return "column does not exist";
case common::E_TYPE_NOT_SUPPORTED:
return "data type not supported";
case common::E_TYPE_NOT_MATCH:
return "data type mismatch";
case common::E_ENCODE_ERR:
return "failed to encode data";
case common::E_DECODE_ERR:
return "failed to decode data";
case common::E_FILE_MAP_ERR:
return "failed to memory-map file";
case common::E_UNSUPPORTED_VERSION:
return "unsupported TsFile format version";
default:
return "internal error";
}
}
OutputFormat resolve_format(ParsedArgs::Format f, bool stdout_is_tty) {
switch (f) {
case ParsedArgs::Format::kCsv:
return OutputFormat::kCsv;
case ParsedArgs::Format::kTsv:
return OutputFormat::kTsv;
case ParsedArgs::Format::kJson:
return OutputFormat::kJson;
case ParsedArgs::Format::kTable:
return OutputFormat::kTable;
case ParsedArgs::Format::kAuto:
default:
(void)stdout_is_tty;
return OutputFormat::kTable;
}
}
const char* tsdatatype_name(common::TSDataType t) {
switch (t) {
case common::BOOLEAN:
return "BOOLEAN";
case common::INT32:
return "INT32";
case common::INT64:
return "INT64";
case common::FLOAT:
return "FLOAT";
case common::DOUBLE:
return "DOUBLE";
case common::TEXT:
return "TEXT";
case common::VECTOR:
return "VECTOR";
case common::UNKNOWN:
return "UNKNOWN";
case common::TIMESTAMP:
return "TIMESTAMP";
case common::DATE:
return "DATE";
case common::BLOB:
return "BLOB";
case common::STRING:
return "STRING";
case common::NULL_TYPE:
return "NULL";
case common::INVALID_DATATYPE:
default:
return "INVALID";
}
}
const char* tsencoding_name(common::TSEncoding e) {
switch (e) {
case common::PLAIN:
return "PLAIN";
case common::DICTIONARY:
return "DICTIONARY";
case common::RLE:
return "RLE";
case common::DIFF:
return "DIFF";
case common::TS_2DIFF:
return "TS_2DIFF";
case common::BITMAP:
return "BITMAP";
case common::GORILLA_V1:
return "GORILLA_V1";
case common::REGULAR:
return "REGULAR";
case common::GORILLA:
return "GORILLA";
case common::ZIGZAG:
return "ZIGZAG";
case common::FREQ:
return "FREQ";
case common::CHIMP:
return "CHIMP";
case common::SPRINTZ:
return "SPRINTZ";
case common::RLBE:
return "RLBE";
case common::CAMEL:
return "CAMEL";
case common::INVALID_ENCODING:
default:
return "UNKNOWN";
}
}
const char* compression_name(common::CompressionType c) {
switch (c) {
case common::UNCOMPRESSED:
return "UNCOMPRESSED";
case common::SNAPPY:
return "SNAPPY";
case common::GZIP:
return "GZIP";
case common::LZO:
return "LZO";
case common::SDT:
return "SDT";
case common::PAA:
return "PAA";
case common::PLA:
return "PLA";
case common::LZ4:
return "LZ4";
case common::ZSTD:
return "ZSTD";
case common::LZMA2:
return "LZMA2";
case common::INVALID_COMPRESSION:
default:
return "UNKNOWN";
}
}
namespace {
// UTF-8 encoding of the replacement character U+FFFD.
constexpr unsigned char kUtf8Replacement[] = {0xef, 0xbf, 0xbd};
template <typename Emit>
void for_each_utf8_byte(const std::string& s, Emit emit) {
size_t i = 0;
while (i < s.size()) {
const unsigned char lead = static_cast<unsigned char>(s[i]);
size_t length = 0;
if (lead <= 0x7f) {
length = 1;
} else if (lead >= 0xc2 && lead <= 0xdf) {
length = 2;
} else if (lead >= 0xe0 && lead <= 0xef) {
length = 3;
} else if (lead >= 0xf0 && lead <= 0xf4) {
length = 4;
}
size_t consumed = 1;
while (consumed < length && consumed < s.size() - i) {
const unsigned char next =
static_cast<unsigned char>(s[i + consumed]);
if (next < 0x80 || next > 0xbf ||
(consumed == 1 && ((lead == 0xe0 && next < 0xa0) ||
(lead == 0xed && next > 0x9f) ||
(lead == 0xf0 && next < 0x90) ||
(lead == 0xf4 && next > 0x8f)))) {
break;
}
++consumed;
}
if (consumed == length) {
for (size_t j = 0; j < consumed; ++j) {
emit(static_cast<unsigned char>(s[i + j]));
}
} else {
for (unsigned char c : kUtf8Replacement) {
emit(c);
}
}
// Consume only a valid prefix of this character, leaving subsequent
// ASCII or valid UTF-8 for the next iteration, even after truncation.
i += consumed;
}
}
} // namespace
std::string replace_invalid_utf8(const std::string& s) {
std::string out;
out.reserve(s.size());
for_each_utf8_byte(
s, [&out](unsigned char c) { out += static_cast<char>(c); });
return out;
}
std::string csv_escape(const std::string& field) {
return common::csv_escape(replace_invalid_utf8(field), ',');
}
std::string json_escape(const std::string& s) {
std::string out;
out.reserve(s.size() + 2);
for_each_utf8_byte(s, [&out](unsigned char c) {
switch (c) {
case '"':
out += "\\\"";
break;
case '\\':
out += "\\\\";
break;
case '\b':
out += "\\b";
break;
case '\f':
out += "\\f";
break;
case '\n':
out += "\\n";
break;
case '\r':
out += "\\r";
break;
case '\t':
out += "\\t";
break;
default:
if (c < 0x20) {
char buf[8];
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
out += buf;
} else {
out += static_cast<char>(c);
}
}
});
return out;
}
// The readable `table` format renders control characters as fixed visible
// escapes: backslash, newline, carriage return and tab become the two-character
// literals \\, \n, \r and \t, and every other non-printable control byte
// (C0 controls plus DEL) becomes \uXXXX. This keeps one logical record on one
// physical line regardless of embedded control characters.
std::string table_escape(const std::string& s) {
std::string out;
out.reserve(s.size() + 2);
for_each_utf8_byte(s, [&out](unsigned char c) {
switch (c) {
case '\\':
out += "\\\\";
break;
case '\n':
out += "\\n";
break;
case '\r':
out += "\\r";
break;
case '\t':
out += "\\t";
break;
default:
// C0 controls (0x00-0x1f) plus DEL (0x7f) are the complete
// set of non-printable control bytes per C iscntrl(). Bytes
// >= 0x80 now belong to well-formed UTF-8 and pass through
// untouched so multi-byte sequences survive.
if (c < 0x20 || c == 0x7f) {
char buf[8];
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
out += buf;
} else {
out += static_cast<char>(c);
}
}
});
return out;
}
namespace {
// FLOAT/DOUBLE cells render non-finite values as nan/inf tokens, which have
// no JSON representation; finite numbers never contain these letters.
bool json_nonfinite(common::TSDataType t, const std::string& cell) {
if (t != common::FLOAT && t != common::DOUBLE) {
return false;
}
return cell.find_first_of("nNiI") != std::string::npos;
}
std::string blob_hex(const std::string& cell) {
static const char kHex[] = "0123456789abcdef";
std::string out;
out.reserve(2 + cell.size() * 2);
out += "0x";
for (unsigned char c : cell) {
out += kHex[c >> 4];
out += kHex[c & 0x0f];
}
return out;
}
} // namespace
std::string validate_column_names(const std::vector<std::string>& names) {
std::set<std::string> seen;
for (const std::string& name : names) {
const std::string display = replace_invalid_utf8(name);
if (!seen.insert(display).second) {
return "duplicate column name after UTF-8 replacement: \"" +
json_escape(display) + "\"";
}
}
return "";
}
bool OutputNameValidator::add_name(std::map<std::string, std::string>& names,
const std::string& name, const char* kind) {
if (!error_.empty()) return false;
const std::string display = replace_invalid_utf8(name);
const auto entry = names.emplace(display, name);
if (!entry.second && entry.first->second != name) {
error_ = std::string("duplicate ") + kind +
" name after UTF-8 replacement: \"" + json_escape(display) +
"\"";
return false;
}
return true;
}
bool OutputNameValidator::add_object(const std::string& name) {
return add_name(objects_, name, "object");
}
bool OutputNameValidator::add_column(const std::string& object,
const std::string& name) {
return add_object(object) && add_name(columns_[object], name, "column");
}
RowWriter::RowWriter(std::ostream& out, OutputFormat fmt,
std::vector<std::string> header,
std::vector<common::TSDataType> types, bool no_header)
: out_(out),
fmt_(fmt),
header_(std::move(header)),
types_(std::move(types)),
no_header_(no_header),
table_widths_(header_.size(), 0) {
error_ = validate_column_names(header_);
if (!error_.empty()) return;
if (fmt_ == OutputFormat::kJson) {
json_keys_.reserve(header_.size());
for (const std::string& name : header_) {
json_keys_.push_back(json_escape(name));
}
}
if (fmt_ == OutputFormat::kTable && !no_header_) {
for (size_t i = 0; i < header_.size(); ++i) {
table_widths_[i] = table_escape(header_[i]).size();
}
}
}
RowWriter::~RowWriter() {
if (spool_ != nullptr) {
std::fclose(spool_);
}
}
bool RowWriter::emits_json_bare(common::TSDataType type) const {
// External NDJSON keeps INT64 and TIMESTAMP as decimal strings so
// JavaScript consumers do not lose precision. Smaller and floating-point
// numeric types remain bare JSON tokens; everything else is quoted.
switch (type) {
case common::BOOLEAN:
case common::INT32:
case common::FLOAT:
case common::DOUBLE:
return true;
default:
return false;
}
}
std::string RowWriter::format_cell(common::TSDataType type,
const std::string& cell) const {
if (type == common::BLOB) {
return blob_hex(cell);
}
return cell;
}
bool RowWriter::ensure_header() {
if (header_done_) {
return out_.good();
}
header_done_ = true;
if (no_header_) {
return out_.good();
}
const char sep = (fmt_ == OutputFormat::kCsv) ? ',' : '\t';
for (size_t i = 0; i < header_.size(); ++i) {
if (i) {
out_ << sep;
}
out_ << (fmt_ == OutputFormat::kCsv ? csv_escape(header_[i])
: header_[i]);
}
out_ << "\n";
return out_.good();
}
bool RowWriter::write(const std::vector<std::string>& cells,
const std::vector<bool>& is_null) {
return write(cells, is_null, types_);
}
bool RowWriter::write(const std::vector<std::string>& cells,
const std::vector<bool>& is_null,
const std::vector<common::TSDataType>& row_types) {
if (!error_.empty() || !out_.good()) {
return false;
}
if (fmt_ == OutputFormat::kTable) {
if (spool_ == nullptr) {
spool_ = std::tmpfile();
if (spool_ == nullptr) {
return false;
}
}
for (size_t i = 0; i < header_.size(); ++i) {
const unsigned char null =
i >= cells.size() || (i < is_null.size() && is_null[i]);
const common::TSDataType type =
i < row_types.size() ? row_types[i] : common::STRING;
const std::string value = null ? std::string() : cells[i];
const uint64_t length = static_cast<uint64_t>(value.size());
const int32_t serialized_type = static_cast<int32_t>(type);
if (std::fwrite(&null, sizeof(null), 1, spool_) != 1 ||
std::fwrite(&serialized_type, sizeof(serialized_type), 1,
spool_) != 1 ||
std::fwrite(&length, sizeof(length), 1, spool_) != 1 ||
(length > 0 &&
std::fwrite(value.data(), 1, length, spool_) != length)) {
return false;
}
table_widths_[i] =
std::max(table_widths_[i],
table_escape(format_cell(type, value)).size());
}
return true;
}
if (fmt_ == OutputFormat::kJson) {
out_ << "{";
for (size_t i = 0; i < header_.size(); ++i) {
if (i) {
out_ << ",";
}
out_ << "\"" << json_keys_[i] << "\":";
const common::TSDataType type =
i < row_types.size() ? row_types[i] : common::STRING;
if (i < is_null.size() && is_null[i]) {
out_ << "null";
} else if (emits_json_bare(type)) {
const std::string cell =
format_cell(type, i < cells.size() ? cells[i] : "");
if (json_nonfinite(type, cell)) {
out_ << "null"; // NaN/Inf: match JSON serializer practice
} else {
out_ << cell;
}
} else {
out_ << "\""
<< json_escape(
format_cell(type, i < cells.size() ? cells[i] : ""))
<< "\"";
}
}
out_ << "}\n";
return out_.good();
}
if (!ensure_header()) {
return false;
}
const char sep = (fmt_ == OutputFormat::kCsv) ? ',' : '\t';
for (size_t i = 0; i < cells.size(); ++i) {
if (i) {
out_ << sep;
}
bool null_cell = i < is_null.size() && is_null[i];
if (null_cell) {
if (fmt_ == OutputFormat::kCsv) {
out_ << "\\N";
}
continue;
}
const common::TSDataType type =
i < row_types.size() ? row_types[i] : common::STRING;
std::string cell = format_cell(type, cells[i]);
if (fmt_ == OutputFormat::kCsv && cell.empty()) {
out_ << "\"\"";
} else if (fmt_ == OutputFormat::kCsv &&
(type == common::STRING || type == common::TEXT) &&
!cell.empty() && cell[0] == '\\') {
cell.insert(0, 1, '\\');
out_ << csv_escape(cell);
} else {
out_ << (fmt_ == OutputFormat::kCsv ? csv_escape(cell) : cell);
}
}
out_ << "\n";
return out_.good();
}
bool RowWriter::finish() {
if (!error_.empty() || !out_.good()) {
return false;
}
if (fmt_ != OutputFormat::kTable) {
if (fmt_ == OutputFormat::kCsv || fmt_ == OutputFormat::kTsv) {
if (!ensure_header()) {
return false;
}
}
out_.flush();
return out_.good();
}
const size_t ncols = header_.size();
auto emit = [&](const std::vector<std::string>& cells,
const std::vector<bool>& nulls,
const std::vector<common::TSDataType>& row_types) {
for (size_t i = 0; i < ncols; ++i) {
std::string cell =
(i < cells.size() && !(i < nulls.size() && nulls[i]))
? table_escape(format_cell(
i < row_types.size() ? row_types[i] : common::STRING,
cells[i]))
: "";
out_ << cell;
if (i + 1 < ncols) {
out_ << std::string(table_widths_[i] - cell.size() + 2, ' ');
}
}
out_ << "\n";
};
if (!no_header_) {
std::vector<bool> no_nulls(ncols, false);
emit(header_, no_nulls,
std::vector<common::TSDataType>(ncols, common::STRING));
}
if (spool_ != nullptr) {
if (std::fflush(spool_) != 0 || std::fseek(spool_, 0, SEEK_SET) != 0) {
return false;
}
while (true) {
std::vector<std::string> cells(ncols);
std::vector<bool> nulls(ncols, false);
std::vector<common::TSDataType> row_types(ncols, common::STRING);
bool eof = false;
for (size_t i = 0; i < ncols; ++i) {
unsigned char null = 0;
int32_t serialized_type = 0;
uint64_t length = 0;
if (std::fread(&null, sizeof(null), 1, spool_) != 1) {
eof = i == 0 && std::feof(spool_);
if (!eof) {
return false;
}
break;
}
if (std::fread(&serialized_type, sizeof(serialized_type), 1,
spool_) != 1 ||
std::fread(&length, sizeof(length), 1, spool_) != 1) {
return false;
}
nulls[i] = null != 0;
row_types[i] = static_cast<common::TSDataType>(serialized_type);
cells[i].resize(static_cast<size_t>(length));
if (length > 0 &&
std::fread(&cells[i][0], 1, length, spool_) != length) {
return false;
}
}
if (eof) {
break;
}
emit(cells, nulls, row_types);
if (!out_.good()) {
return false;
}
}
std::fclose(spool_);
spool_ = nullptr;
}
out_.flush();
return out_.good();
}
} // namespace tsfile_cli