blob: 828bb6c55269570713571d493454c8d81ebb10df [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 <algorithm>
#include <climits>
#include <limits>
#include <memory>
#include <regex>
#include <set>
#include <sstream>
#include <string>
#include <vector>
#include "cli/exit_codes.h"
#include "commands/commands.h"
#include "common/device_id.h"
#include "common/schema.h"
#include "format/result_set_format.h"
#include "reader/filter/tag_filter.h"
#include "reader/tsfile_reader.h"
namespace tsfile_cli {
namespace {
class CliFullMatchTagRegExp : public storage::TagFilter {
public:
CliFullMatchTagRegExp(int col_idx, const std::string& pattern)
: storage::TagFilter(col_idx, pattern), pattern_(pattern) {}
bool satisfyRow(std::vector<std::string*> segments) const override {
if (col_idx_ >= segments.size() || segments[col_idx_] == nullptr) {
return false;
}
return std::regex_match(*segments[col_idx_], pattern_);
}
private:
std::regex pattern_;
};
bool can_push_down_row_window(const ParsedArgs& args, long long offset,
long long limit) {
return !args.has_start && !args.has_end && offset == 0 &&
(limit < 0 || limit <= INT_MAX);
}
int to_reader_row_bound(long long value) {
return value < 0 ? -1 : static_cast<int>(value);
}
int resolve_table_fields(const ParsedArgs& args,
const std::shared_ptr<storage::TableSchema>& schema,
std::vector<std::string>& fields, std::ostream& err) {
if (!schema) {
err << "Error: table '" << args.table << "' does not exist\n";
return kExitUsage;
}
auto measurements = schema->get_measurement_schemas();
auto categories = schema->get_column_categories();
for (size_t i = 0; i < measurements.size(); ++i) {
if (i < categories.size() && measurements[i] &&
categories[i] == common::ColumnCategory::TAG) {
fields.push_back(measurements[i]->measurement_name_);
}
}
if (args.measurements.empty()) {
for (size_t i = 0; i < measurements.size(); ++i) {
if (i < categories.size() && measurements[i] &&
categories[i] == common::ColumnCategory::FIELD) {
fields.push_back(measurements[i]->measurement_name_);
}
}
return kExitOk;
}
for (const std::string& requested : args.measurements) {
const int index = schema->find_column_index(requested);
if (index < 0 || static_cast<size_t>(index) >= measurements.size() ||
!measurements[index]) {
err << "Error: FIELD '" << requested
<< "' does not exist in table '" << schema->get_table_name()
<< "'\n";
return kExitUsage;
}
const common::ColumnCategory category =
static_cast<size_t>(index) < categories.size()
? categories[index]
: common::ColumnCategory::FIELD;
if (category != common::ColumnCategory::FIELD) {
err << "Error: column '" << requested
<< "' is not a FIELD in table '" << schema->get_table_name()
<< "'\n";
return kExitUsage;
}
fields.push_back(measurements[index]->measurement_name_);
}
return kExitOk;
}
int resolve_tree_paths(const ParsedArgs& args, storage::TsFileReader& reader,
std::vector<std::string>& paths, std::ostream& err) {
auto devices = reader.get_all_device_ids();
std::shared_ptr<storage::IDeviceID> target_device;
for (const auto& device : devices) {
if (device && device->get_device_name() == args.device) {
target_device = device;
break;
}
}
if (!target_device) {
err << "Error: device '" << args.device << "' does not exist\n";
return kExitUsage;
}
std::vector<storage::MeasurementSchema> schemas;
if (reader.get_timeseries_schema(target_device, schemas) != common::E_OK) {
err << "Error: failed to read schema for device '" << args.device
<< "'\n";
return kExitFile;
}
std::set<std::string> known;
for (const auto& schema : schemas) {
known.insert(schema.measurement_name_);
}
for (const std::string& requested : args.measurements) {
if (known.find(requested) == known.end()) {
err << "Error: FIELD '" << requested
<< "' does not exist in device '" << args.device << "'\n";
return kExitUsage;
}
}
paths = collect_tree_query_paths(args, reader);
if (paths.empty()) {
err << "Error: device '" << args.device << "' has no FIELD columns\n";
return kExitUsage;
}
return kExitOk;
}
} // namespace
std::unique_ptr<storage::Filter> build_table_tag_filter(
const ParsedArgs& args, storage::TsFileReader& reader,
const std::string& table_name, std::ostream& err) {
if (!args.has_tag_filter) {
return std::unique_ptr<storage::Filter>();
}
auto schema = reader.get_table_schema(table_name);
if (!schema) {
err << "Error: no schema found for table " << table_name << "\n";
return std::unique_ptr<storage::Filter>();
}
storage::TagFilterBuilder builder(schema.get());
std::unique_ptr<storage::Filter> combined;
for (const ParsedArgs::TagFilterSpec& spec : args.tag_filters) {
storage::Filter* filter = nullptr;
switch (spec.op) {
case ParsedArgs::TagFilterOp::kEq:
filter = builder.eq(spec.column, spec.value);
break;
case ParsedArgs::TagFilterOp::kNeq:
filter = builder.neq(spec.column, spec.value);
break;
case ParsedArgs::TagFilterOp::kRegexp:
try {
std::regex pattern(spec.value);
(void)pattern;
} catch (const std::regex_error&) {
err << "Error: invalid regular expression for TAG '"
<< spec.column << "'\n";
return std::unique_ptr<storage::Filter>();
}
{
int tag_order = schema->find_id_column_order(spec.column);
if (tag_order >= 0) {
filter = new CliFullMatchTagRegExp(tag_order + 1,
spec.value);
}
}
break;
case ParsedArgs::TagFilterOp::kIsNull:
filter = builder.is_null(spec.column);
break;
case ParsedArgs::TagFilterOp::kNotNull:
filter = builder.is_not_null(spec.column);
break;
case ParsedArgs::TagFilterOp::kNone:
break;
}
if (filter == nullptr) {
err << "Error: invalid tag filter column '" << spec.column
<< "' for table " << table_name << "\n";
return std::unique_ptr<storage::Filter>();
}
if (!combined) {
combined.reset(filter);
} else if (args.tag_match == "any") {
combined.reset(storage::TagFilterBuilder::or_filter(
combined.release(), filter));
} else {
combined.reset(storage::TagFilterBuilder::and_filter(
combined.release(), filter));
}
}
return combined;
}
std::vector<std::string> collect_tree_query_paths(
const ParsedArgs& args, storage::TsFileReader& reader) {
std::vector<std::string> paths;
const bool has_projection = !args.measurements.empty();
auto include_measurement = [&](const std::string& m) {
return !has_projection ||
std::find(args.measurements.begin(), args.measurements.end(),
m) != args.measurements.end();
};
if (!args.device.empty()) {
// A single device was requested: resolve its series and keep only the
// ones matching the projection. Filtering against the device's real
// schema means a provided measurement that doesn't exist on the device
// is dropped rather than queried blindly (matching the no-device path).
auto did = std::make_shared<storage::StringArrayDeviceID>(args.device);
std::vector<storage::MeasurementSchema> sch;
if (reader.get_timeseries_schema(did, sch) == 0) {
for (auto& m : sch) {
if (include_measurement(m.measurement_name_)) {
paths.push_back(args.device + "." + m.measurement_name_);
}
}
}
return paths;
}
// No device filter: collect every device/series from a single whole-file
// metadata call instead of querying each device one by one.
storage::DeviceTimeseriesMetadataMap meta =
reader.get_timeseries_metadata();
for (auto& kv : meta) {
if (!kv.first) {
continue;
}
const std::string dev = kv.first->get_device_name();
for (auto& ts : kv.second) {
if (!ts) {
continue;
}
const std::string m = ts->get_measurement_name().to_std_string();
if (include_measurement(m)) {
paths.push_back(dev + "." + m);
}
}
}
return paths;
}
int run_row_query(const ParsedArgs& args, storage::TsFileReader& reader,
OutputFormat fmt, std::ostream& out, std::ostream& err,
long long offset, long long limit, long long* emitted_rows) {
const int64_t start = args.has_start ? static_cast<int64_t>(args.start)
: std::numeric_limits<int64_t>::min();
const int64_t end = args.has_end ? static_cast<int64_t>(args.end)
: std::numeric_limits<int64_t>::max();
storage::ResultSet* rs = nullptr;
int qret = 0;
const bool push_down = can_push_down_row_window(args, offset, limit);
std::unique_ptr<storage::Filter> tag_filter;
if (is_table_model(args, reader)) {
std::string table_name = args.table;
if (table_name.empty()) {
auto schemas = reader.get_all_table_schemas();
if (schemas.empty() || !schemas[0]) {
err << "Error: no table found in file\n";
return kExitRuntime;
}
if (schemas.size() != 1) {
err << "Error: head/cat requires -t/--table when the file "
"contains multiple tables\n";
return kExitUsage;
}
table_name = schemas[0]->get_table_name();
}
auto table_schema = reader.get_table_schema(table_name);
std::vector<std::string> cols;
int selection_ret = resolve_table_fields(args, table_schema, cols, err);
if (selection_ret != kExitOk) {
return selection_ret;
}
tag_filter = build_table_tag_filter(args, reader, table_name, err);
if (args.has_tag_filter && tag_filter == nullptr) {
return kExitUsage;
}
if (push_down) {
qret = reader.queryByRow(
table_name, cols, to_reader_row_bound(offset),
to_reader_row_bound(limit), rs, tag_filter.get());
} else {
qret = reader.query(table_name, cols, start, end, rs,
tag_filter.get());
}
} else {
if (args.has_tag_filter) {
err << "Error: tag filter flags are only valid for table model\n";
return kExitUsage;
}
ParsedArgs effective_args = args;
if (effective_args.device.empty()) {
auto devices = reader.get_all_device_ids();
if (devices.empty() || !devices[0]) {
err << "Error: no device found in file\n";
return kExitRuntime;
}
if (devices.size() != 1) {
err << "Error: head/cat requires -d/--device when the file "
"contains multiple devices\n";
return kExitUsage;
}
effective_args.device = devices[0]->get_device_name();
}
std::vector<std::string> paths;
int selection_ret =
resolve_tree_paths(effective_args, reader, paths, err);
if (selection_ret != kExitOk) {
return selection_ret;
}
if (push_down) {
qret = reader.queryByRow(paths, to_reader_row_bound(offset),
to_reader_row_bound(limit), rs);
} else {
qret = reader.query(paths, start, end, rs);
}
}
if (qret != 0 || rs == nullptr) {
err << "Error: query failed: " << error_code_message(qret) << "\n";
if (rs != nullptr) {
reader.destroy_query_data_set(rs);
}
return kExitFile;
}
// Stage the complete result before publishing it. A decode/read failure
// can occur after the result header or earlier rows have been rendered;
// input failures must not leave a partial machine-readable stdout stream
// that callers could mistake for a complete result. The final write is
// still checked separately so stdout errors remain runtime failures.
std::ostringstream staged;
int wret = push_down ? emit_result_set(rs, fmt, args.no_header, staged, 0,
-1, emitted_rows)
: emit_result_set(rs, fmt, args.no_header, staged,
offset, limit, emitted_rows);
reader.destroy_query_data_set(rs);
if (wret == common::E_OK) {
const std::string bytes = staged.str();
out.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
out.flush();
if (!out.good()) {
wret = common::E_FILE_WRITE_ERR;
}
}
if (wret == common::E_OUT_OF_RANGE) {
err << "Error: offset exceeds matched row count\n";
return kExitUsage;
}
if (wret != 0) {
err << "Error: failed to read rows: " << error_code_message(wret)
<< "\n";
return wret == common::E_FILE_WRITE_ERR ? kExitRuntime : kExitFile;
}
return kExitOk;
}
} // namespace tsfile_cli