blob: a30e2da9374a97d125797c70ca87907db0034ede [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 <array>
#include <memory>
#include <string>
#include <vector>
#include <arrow/filesystem/localfs.h>
#include <arrow/result.h>
#include <arrow/status.h>
#include <gtest/gtest.h>
#include "iceberg/arrow/arrow_io_internal.h"
#include "iceberg/test/matchers.h"
#include "iceberg/test/std_io.h"
#include "iceberg/test/temp_file_test_base.h"
namespace iceberg {
namespace {
std::string ForeignSchemeUri(std::string local_path) {
std::ranges::replace(local_path, '\\', '/');
return "x-store://" + local_path;
}
struct CloseState {
bool closed = false;
};
class ReadFailureInputStream : public SeekableInputStream {
public:
explicit ReadFailureInputStream(std::shared_ptr<CloseState> state)
: state_(std::move(state)) {}
Result<int64_t> Position() const override { return 0; }
Status Seek(int64_t /*position*/) override { return {}; }
Result<int64_t> Read(std::span<std::byte> /*out*/) override { return 0; }
Status ReadFully(int64_t /*position*/, std::span<std::byte> /*out*/) override {
return IOError("read failed");
}
Status Close() override {
state_->closed = true;
return IOError("close failed");
}
private:
std::shared_ptr<CloseState> state_;
};
class ReadFailureInputFile : public InputFile {
public:
explicit ReadFailureInputFile(std::shared_ptr<CloseState> state)
: state_(std::move(state)) {}
std::string_view location() const override { return "read-failure"; }
Result<int64_t> Size() const override { return 4; }
Result<std::unique_ptr<SeekableInputStream>> Open() override {
return std::make_unique<ReadFailureInputStream>(state_);
}
private:
std::shared_ptr<CloseState> state_;
};
class ReadFailureFileIO : public FileIO {
public:
explicit ReadFailureFileIO(std::shared_ptr<CloseState> state)
: state_(std::move(state)) {}
Result<std::unique_ptr<InputFile>> NewInputFile(
std::string /*file_location*/) override {
return std::make_unique<ReadFailureInputFile>(state_);
}
private:
std::shared_ptr<CloseState> state_;
};
class WriteFailureOutputStream : public PositionOutputStream {
public:
explicit WriteFailureOutputStream(std::shared_ptr<CloseState> state)
: state_(std::move(state)) {}
Result<int64_t> Position() const override { return 0; }
Status Write(std::span<const std::byte> /*data*/) override {
return IOError("write failed");
}
Status Flush() override { return {}; }
Status Close() override {
state_->closed = true;
return IOError("close failed");
}
private:
std::shared_ptr<CloseState> state_;
};
class WriteFailureOutputFile : public OutputFile {
public:
explicit WriteFailureOutputFile(std::shared_ptr<CloseState> state)
: state_(std::move(state)) {}
std::string_view location() const override { return "write-failure"; }
Result<std::unique_ptr<PositionOutputStream>> Create() override {
return std::make_unique<WriteFailureOutputStream>(state_);
}
Result<std::unique_ptr<PositionOutputStream>> CreateOrOverwrite() override {
return std::make_unique<WriteFailureOutputStream>(state_);
}
private:
std::shared_ptr<CloseState> state_;
};
class WriteFailureFileIO : public FileIO {
public:
explicit WriteFailureFileIO(std::shared_ptr<CloseState> state)
: state_(std::move(state)) {}
Result<std::unique_ptr<OutputFile>> NewOutputFile(
std::string /*file_location*/) override {
return std::make_unique<WriteFailureOutputFile>(state_);
}
private:
std::shared_ptr<CloseState> state_;
};
struct PermissiveReadState {
std::string data;
bool closed = false;
int64_t position = 0;
};
class PermissiveInputStream : public SeekableInputStream {
public:
explicit PermissiveInputStream(std::shared_ptr<PermissiveReadState> state)
: state_(std::move(state)) {}
Result<int64_t> Position() const override { return state_->position; }
Status Seek(int64_t position) override {
if (position < 0) {
return InvalidArgument("Cannot seek to negative position {}", position);
}
state_->position = position;
return {};
}
Result<int64_t> Read(std::span<std::byte> out) override {
auto position = static_cast<size_t>(state_->position);
if (position >= state_->data.size()) {
return 0;
}
auto bytes_to_read = std::min(out.size(), state_->data.size() - position);
std::copy_n(reinterpret_cast<const std::byte*>(state_->data.data() + position),
bytes_to_read, out.data());
state_->position += static_cast<int64_t>(bytes_to_read);
return static_cast<int64_t>(bytes_to_read);
}
Status ReadFully(int64_t position, std::span<std::byte> out) override {
if (position < 0) {
return InvalidArgument("Cannot read from negative position {}", position);
}
auto offset = static_cast<size_t>(position);
if (offset > state_->data.size() || out.size() > state_->data.size() - offset) {
return IOError("Unexpected EOF");
}
std::copy_n(reinterpret_cast<const std::byte*>(state_->data.data() + offset),
out.size(), out.data());
return {};
}
Status Close() override {
state_->closed = true;
return {};
}
private:
std::shared_ptr<PermissiveReadState> state_;
};
class PermissiveInputFile : public InputFile {
public:
explicit PermissiveInputFile(std::shared_ptr<PermissiveReadState> state)
: state_(std::move(state)) {}
std::string_view location() const override { return "permissive-input"; }
Result<int64_t> Size() const override {
return static_cast<int64_t>(state_->data.size());
}
Result<std::unique_ptr<SeekableInputStream>> Open() override {
return std::make_unique<PermissiveInputStream>(state_);
}
private:
std::shared_ptr<PermissiveReadState> state_;
};
class PermissiveInputFileIO : public FileIO {
public:
explicit PermissiveInputFileIO(std::shared_ptr<PermissiveReadState> state)
: state_(std::move(state)) {}
Result<std::unique_ptr<InputFile>> NewInputFile(
std::string /*file_location*/) override {
return std::make_unique<PermissiveInputFile>(state_);
}
private:
std::shared_ptr<PermissiveReadState> state_;
};
struct PermissiveWriteState {
std::string data;
bool closed = false;
};
class PermissiveOutputStream : public PositionOutputStream {
public:
explicit PermissiveOutputStream(std::shared_ptr<PermissiveWriteState> state)
: state_(std::move(state)) {}
Result<int64_t> Position() const override {
return static_cast<int64_t>(state_->data.size());
}
Status Write(std::span<const std::byte> data) override {
state_->data.append(reinterpret_cast<const char*>(data.data()), data.size());
return {};
}
Status Flush() override { return {}; }
Status Close() override {
state_->closed = true;
return {};
}
private:
std::shared_ptr<PermissiveWriteState> state_;
};
class PermissiveOutputFile : public OutputFile {
public:
explicit PermissiveOutputFile(std::shared_ptr<PermissiveWriteState> state)
: state_(std::move(state)) {}
std::string_view location() const override { return "permissive-output"; }
Result<std::unique_ptr<PositionOutputStream>> Create() override {
return std::make_unique<PermissiveOutputStream>(state_);
}
Result<std::unique_ptr<PositionOutputStream>> CreateOrOverwrite() override {
return std::make_unique<PermissiveOutputStream>(state_);
}
private:
std::shared_ptr<PermissiveWriteState> state_;
};
class PermissiveOutputFileIO : public FileIO {
public:
explicit PermissiveOutputFileIO(std::shared_ptr<PermissiveWriteState> state)
: state_(std::move(state)) {}
Result<std::unique_ptr<OutputFile>> NewOutputFile(
std::string /*file_location*/) override {
return std::make_unique<PermissiveOutputFile>(state_);
}
private:
std::shared_ptr<PermissiveWriteState> state_;
};
} // namespace
class LocalFileIOTest : public TempFileTestBase {
protected:
void SetUp() override {
TempFileTestBase::SetUp();
file_io_ = std::make_shared<iceberg::arrow::ArrowFileSystemFileIO>(
std::make_shared<::arrow::fs::LocalFileSystem>());
temp_filepath_ = CreateNewTempFilePath();
}
std::shared_ptr<iceberg::FileIO> file_io_;
std::string temp_filepath_;
};
TEST_F(LocalFileIOTest, ReadWriteFile) {
auto read_res = file_io_->ReadFile(temp_filepath_, std::nullopt);
EXPECT_THAT(read_res, IsError(ErrorKind::kIOError));
EXPECT_THAT(read_res, HasErrorMessage("Failed to open local file"));
auto write_res = file_io_->WriteFile(temp_filepath_, "hello world");
EXPECT_THAT(write_res, IsOk());
read_res = file_io_->ReadFile(temp_filepath_, std::nullopt);
EXPECT_THAT(read_res, IsOk());
EXPECT_THAT(read_res, HasValue(::testing::Eq("hello world")));
}
TEST_F(LocalFileIOTest, DeleteFile) {
auto write_res = file_io_->WriteFile(temp_filepath_, "hello world");
EXPECT_THAT(write_res, IsOk());
auto del_res = file_io_->DeleteFile(temp_filepath_);
EXPECT_THAT(del_res, IsOk());
del_res = file_io_->DeleteFile(temp_filepath_);
EXPECT_THAT(del_res, IsError(ErrorKind::kIOError));
EXPECT_THAT(del_res, HasErrorMessage("Cannot delete file"));
}
TEST_F(LocalFileIOTest, DeleteFiles) {
auto first_path = CreateNewTempFilePath();
auto second_path = CreateNewTempFilePath();
ASSERT_THAT(file_io_->WriteFile(first_path, "hello"), IsOk());
ASSERT_THAT(file_io_->WriteFile(second_path, "world"), IsOk());
std::vector<std::string> paths = {first_path, second_path};
EXPECT_THAT(file_io_->DeleteFiles(paths), IsOk());
EXPECT_THAT(file_io_->ReadFile(first_path, std::nullopt), IsError(ErrorKind::kIOError));
EXPECT_THAT(file_io_->ReadFile(second_path, std::nullopt),
IsError(ErrorKind::kIOError));
}
void VerifyReadFullyReadsFromAbsolutePosition(const std::shared_ptr<FileIO>& file_io,
const std::string& path) {
ASSERT_THAT(file_io->WriteFile(path, "abcdef"), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto input_file, file_io->NewInputFile(path));
ICEBERG_UNWRAP_OR_FAIL(auto stream, input_file->Open());
ASSERT_THAT(stream->Seek(5), IsOk());
std::array<std::byte, 2> buffer;
ASSERT_THAT(stream->ReadFully(1, buffer), IsOk());
std::string data(reinterpret_cast<const char*>(buffer.data()), buffer.size());
EXPECT_EQ(data, "bc");
ASSERT_THAT(stream->Seek(5), IsOk());
std::array<std::byte, 1> next;
ICEBERG_UNWRAP_OR_FAIL(auto bytes_read, stream->Read(next));
ASSERT_EQ(bytes_read, 1);
EXPECT_EQ(next[0], std::byte{'f'});
}
TEST_F(LocalFileIOTest, ReadFullyReadsFromAbsolutePosition) {
ASSERT_NO_FATAL_FAILURE(
VerifyReadFullyReadsFromAbsolutePosition(file_io_, temp_filepath_));
}
TEST_F(LocalFileIOTest, StdReadFullyReadsFromAbsolutePosition) {
auto file_io = std::make_shared<test::StdFileIO>();
ASSERT_NO_FATAL_FAILURE(
VerifyReadFullyReadsFromAbsolutePosition(file_io, temp_filepath_));
}
TEST_F(LocalFileIOTest, StdReadKeepsPositionAvailableAtEof) {
auto file_io = std::make_shared<test::StdFileIO>();
ASSERT_THAT(file_io->WriteFile(temp_filepath_, "abc"), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto input_file, file_io->NewInputFile(temp_filepath_));
ICEBERG_UNWRAP_OR_FAIL(auto stream, input_file->Open());
std::array<std::byte, 8> buffer;
ICEBERG_UNWRAP_OR_FAIL(auto bytes_read, stream->Read(buffer));
EXPECT_EQ(bytes_read, 3);
EXPECT_THAT(stream->Position(), HasValue(::testing::Eq(3)));
ICEBERG_UNWRAP_OR_FAIL(bytes_read, stream->Read(buffer));
EXPECT_EQ(bytes_read, 0);
EXPECT_THAT(stream->Position(), HasValue(::testing::Eq(3)));
}
TEST(ArrowFileIOTest, OutputStoredLengthAfterClose) {
auto file_io = arrow::ArrowFileSystemFileIO::MakeMockFileIO();
ICEBERG_UNWRAP_OR_FAIL(auto output_file, file_io->NewOutputFile("output"));
ICEBERG_UNWRAP_OR_FAIL(auto output, output_file->Create());
std::array<std::byte, 3> data = {std::byte{'a'}, std::byte{'b'}, std::byte{'c'}};
ASSERT_THAT(output->Write(data), IsOk());
ASSERT_THAT(output->Close(), IsOk());
auto position = output->Position();
ASSERT_FALSE(position.has_value());
EXPECT_THAT(position.error().message, ::testing::HasSubstr("closed"));
EXPECT_THAT(output->StoredLength(), HasValue(::testing::Eq(3)));
}
TEST_F(LocalFileIOTest, ResolvesForeignSchemeToUnderlyingPath) {
ASSERT_THAT(file_io_->WriteFile(temp_filepath_, "hello world"), IsOk());
auto read_res = file_io_->ReadFile(ForeignSchemeUri(temp_filepath_), std::nullopt);
EXPECT_THAT(read_res, IsOk());
EXPECT_THAT(read_res, HasValue(::testing::Eq("hello world")));
auto with_query = file_io_->ReadFile(ForeignSchemeUri(temp_filepath_) + "?versionId=42",
std::nullopt);
EXPECT_THAT(with_query, IsOk());
EXPECT_THAT(with_query, HasValue(::testing::Eq("hello world")));
}
TEST_F(LocalFileIOTest, PropagatesNonSchemeMismatchUriError) {
auto read_res = file_io_->ReadFile("file:///tmp/%ZZ", std::nullopt);
EXPECT_THAT(read_res, IsError(ErrorKind::kUnknownError));
EXPECT_THAT(read_res, HasErrorMessage("Cannot parse URI"));
}
TEST_F(LocalFileIOTest, FallbackDecodesPercentEncodingInKey) {
std::string decoded_path = temp_filepath_ + " x";
ASSERT_THAT(file_io_->WriteFile(decoded_path, "raw"), IsOk());
auto read_res =
file_io_->ReadFile(ForeignSchemeUri(temp_filepath_ + "%20x"), std::nullopt);
EXPECT_THAT(read_res, IsOk());
EXPECT_THAT(read_res, HasValue(::testing::Eq("raw")));
}
TEST(FileIOAdapterTest, InputAdapterRejectsReadsAfterClose) {
auto state = std::make_shared<PermissiveReadState>();
state->data = "abc";
auto file_io = std::make_shared<PermissiveInputFileIO>(state);
ICEBERG_UNWRAP_OR_FAIL(auto input, arrow::OpenArrowInputStream(file_io, "input"));
ASSERT_TRUE(input->Close().ok());
ASSERT_TRUE(input->Close().ok());
ASSERT_TRUE(state->closed);
std::array<std::byte, 1> out;
auto result = input->Read(static_cast<int64_t>(out.size()), out.data());
auto read_at_result = input->ReadAt(0, static_cast<int64_t>(out.size()), out.data());
EXPECT_FALSE(result.ok());
EXPECT_THAT(result.status().ToString(), ::testing::HasSubstr("closed"));
EXPECT_FALSE(read_at_result.ok());
EXPECT_THAT(read_at_result.status().ToString(), ::testing::HasSubstr("closed"));
EXPECT_EQ(state->position, 0);
}
TEST(FileIOAdapterTest, InputAdapterRejectsReadAtBeyondKnownSize) {
auto state = std::make_shared<PermissiveReadState>();
state->data = "abc";
auto file_io = std::make_shared<PermissiveInputFileIO>(state);
ICEBERG_UNWRAP_OR_FAIL(auto input, arrow::OpenArrowInputStream(file_io, "input"));
std::array<std::byte, 1> out;
auto read_at_end = input->ReadAt(3, static_cast<int64_t>(out.size()), out.data());
auto read_past_end = input->ReadAt(4, static_cast<int64_t>(out.size()), out.data());
ASSERT_TRUE(read_at_end.ok());
EXPECT_EQ(read_at_end.ValueOrDie(), 0);
EXPECT_FALSE(read_past_end.ok());
EXPECT_THAT(read_past_end.status().ToString(), ::testing::HasSubstr("out of bounds"));
}
TEST(FileIOAdapterTest, InputAdapterUsesInputFileSizeWithLengthHint) {
auto state = std::make_shared<PermissiveReadState>();
state->data = "abc";
auto file_io = std::make_shared<PermissiveInputFileIO>(state);
ICEBERG_UNWRAP_OR_FAIL(auto input, arrow::OpenArrowInputStream(file_io, "input", 99));
auto size = input->GetSize();
ASSERT_TRUE(size.ok()) << size.status().ToString();
EXPECT_EQ(size.ValueOrDie(), 3);
}
TEST(FileIOAdapterTest, OutputAdapterRejectsWritesAfterClose) {
auto state = std::make_shared<PermissiveWriteState>();
auto file_io = std::make_shared<PermissiveOutputFileIO>(state);
ICEBERG_UNWRAP_OR_FAIL(auto output, arrow::OpenArrowOutputStream(file_io, "output"));
ASSERT_TRUE(output->Close().ok());
ASSERT_TRUE(output->Close().ok());
ASSERT_TRUE(state->closed);
auto status = output->Write("x", 1);
auto flush_status = output->Flush();
EXPECT_FALSE(status.ok());
EXPECT_THAT(status.ToString(), ::testing::HasSubstr("closed"));
EXPECT_FALSE(flush_status.ok());
EXPECT_THAT(flush_status.ToString(), ::testing::HasSubstr("closed"));
EXPECT_TRUE(state->data.empty());
}
TEST(FileIOTest, ReadFileReturnsReadErrorWithCloseContext) {
auto state = std::make_shared<CloseState>();
ReadFailureFileIO file_io(state);
auto result = file_io.ReadFile("read-failure", std::nullopt);
EXPECT_TRUE(state->closed);
EXPECT_THAT(result, IsError(ErrorKind::kIOError));
EXPECT_THAT(result, HasErrorMessage("read failed"));
EXPECT_THAT(result, HasErrorMessage("close failed"));
}
TEST(FileIOTest, WriteFileReturnsWriteErrorWithCloseContext) {
auto state = std::make_shared<CloseState>();
WriteFailureFileIO file_io(state);
auto result = file_io.WriteFile("write-failure", "data");
EXPECT_TRUE(state->closed);
EXPECT_THAT(result, IsError(ErrorKind::kIOError));
EXPECT_THAT(result, HasErrorMessage("write failed"));
EXPECT_THAT(result, HasErrorMessage("close failed"));
}
} // namespace iceberg