Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .clangd
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
If:
PathMatch: src/.*

CompileFlags:
CompilationDatabase: build/release

---
If:
PathMatch: test/.*

CompileFlags:
CompilationDatabase: build/unit_tests
3 changes: 3 additions & 0 deletions docs/pages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ COPY <table_name> TO '11QdEasMWbETbFVxry-SsD8jVcdYIT1zBQszcF84MdE8' (FORMAT gshe
-- Write a spreadsheet from a table by full URL
COPY <table_name> TO 'https://docs.google.com/spreadsheets/d/11QdEasMWbETbFVxry-SsD8jVcdYIT1zBQszcF84MdE8/edit?usp=sharing' (FORMAT gsheet);

-- Create a sheet if it doesn't already exist
COPY <table_name> TO 'https://docs.google.com/spreadsheets/d/11QdEasMWbETbFVxry-SsD8jVcdYIT1zBQszcF84MdE8/edit' (FORMAT gsheet, sheet 'Woot', create_if_not_exists true);

-- Write a spreadsheet to a specific sheet using the sheet id in the URL
COPY <table_name> TO 'https://docs.google.com/spreadsheets/d/11QdEasMWbETbFVxry-SsD8jVcdYIT1zBQszcF84MdE8/edit?gid=1295634987#gid=1295634987' (FORMAT gsheet);

Expand Down
158 changes: 64 additions & 94 deletions src/gsheets_copy.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
#include <vector>
#include <utility>

#include "duckdb/common/case_insensitive_map.hpp"
#include "duckdb/common/exception.hpp"
#include "duckdb/common/exception/binder_exception.hpp"
#include "duckdb/common/file_system.hpp"
#include "duckdb/common/types.hpp"
#include "duckdb/common/types/value.hpp"

#include "gsheets_copy.hpp"
#include "gsheets_utils.hpp"

#include "sheets/auth_factory.hpp"
#include "sheets/client.hpp"
#include "sheets/exception.hpp"
#include "sheets/range.hpp"
#include "sheets/transport/client_factory.hpp"
#include "sheets/types.hpp"
Expand All @@ -22,110 +26,66 @@ GSheetCopyFunction::GSheetCopyFunction() : CopyFunction("gsheet") {
copy_to_sink = GSheetWriteSink;
}

static std::string GetStringOption(const case_insensitive_map_t<vector<Value>> &options, const std::string &name,
const std::string &default_value = "") {
const auto it = options.find(name);
if (it == options.end()) {
return default_value;
}
std::string err;
Value val;
if (!it->second.back().DefaultTryCastAs(LogicalType::VARCHAR, val, &err)) {
throw BinderException(name + " option must be VARCHAR");
}
if (val.IsNull()) {
throw BinderException(name + " option must not be NULL");
}
return StringValue::Get(val);
}

// NOTE: the second value in pair is a flag indicating if the value was set by the user
static std::pair<bool, bool> GetBoolOption(const case_insensitive_map_t<vector<Value>> &options,
const std::string &name, bool default_value = false) {
const auto it = options.find(name);
if (it == options.end()) {
return std::make_pair(default_value, false);
}
if (it->second.size() != 1) {
throw BinderException(name + " option must be a single boolean value");
}
std::string err;
Value val;
if (!it->second.back().DefaultTryCastAs(LogicalType::BOOLEAN, val, &err)) {
throw BinderException(name + " option must be a single boolean value");
}
if (val.IsNull()) {
throw BinderException(name + " option must be a single boolean value");
}
return std::make_pair(BooleanValue::Get(val), true);
}

unique_ptr<FunctionData> GSheetCopyFunction::GSheetWriteBind(ClientContext &context, CopyFunctionBindInput &input,
const vector<string> &names,
const vector<LogicalType> &sql_types) {
string file_path = input.info.file_path;

string file_path = input.info.file_path;
auto options = input.info.options;

const auto sheet_opt = options.find("sheet");
std::string sheet;
if (sheet_opt != options.end()) {
string error_msg;
Value sheet_val;
if (!sheet_opt->second.back().DefaultTryCastAs(LogicalType::VARCHAR, sheet_val, &error_msg)) {
throw BinderException("sheet option must be a VARCHAR");
}
if (sheet_val.IsNull()) {
throw BinderException("sheet option must be a non-null VARCHAR");
}
sheet = StringValue::Get(sheet_val);
} else {
sheet = "";
}
auto sheet = GetStringOption(options, "sheet");
auto range = GetStringOption(options, "range");
bool overwrite_sheet = GetBoolOption(options, "overwrite_sheet", true).first;
bool overwrite_range = GetBoolOption(options, "overwrite_range", false).first;
bool create_if_not_exists = GetBoolOption(options, "create_if_not_exists", false).first;

const auto range_opt = options.find("range");
std::string range;
if (range_opt != options.end()) {
string error_msg;
Value range_val;
if (!range_opt->second.back().DefaultTryCastAs(LogicalType::VARCHAR, range_val, &error_msg)) {
throw BinderException("range option must be a VARCHAR");
}
if (range_val.IsNull()) {
throw BinderException("range option must be a non-null VARCHAR");
}
range = StringValue::Get(range_val);
} else {
range = "";
}
auto header_result = GetBoolOption(options, "header", true);
bool header = header_result.second ? header_result.first : (overwrite_range || overwrite_sheet);

const auto overwrite_sheet_opt = options.find("overwrite_sheet");
bool overwrite_sheet;
if (overwrite_sheet_opt != options.end()) {
if (overwrite_sheet_opt->second.size() != 1) {
throw BinderException("overwrite_sheet option must be a single boolean value");
}
string error_msg;
Value overwrite_sheet_bool_val;
if (!overwrite_sheet_opt->second.back().DefaultTryCastAs(LogicalType::BOOLEAN, overwrite_sheet_bool_val,
&error_msg)) {
throw BinderException("overwrite_sheet option must be a single boolean value");
}
if (overwrite_sheet_bool_val.IsNull()) {
throw BinderException("overwrite_sheet option must be a single boolean value");
}
overwrite_sheet = BooleanValue::Get(overwrite_sheet_bool_val);
} else {
overwrite_sheet = true; // Default to overwrite_sheet to maintain prior behavior
}

const auto overwrite_range_opt = options.find("overwrite_range");
bool overwrite_range;
if (overwrite_range_opt != options.end()) {
if (overwrite_range_opt->second.size() != 1) {
throw BinderException("overwrite_range option must be a single boolean value");
}
string error_msg;
Value overwrite_range_bool_val;
if (!overwrite_range_opt->second.back().DefaultTryCastAs(LogicalType::BOOLEAN, overwrite_range_bool_val,
&error_msg)) {
throw BinderException("overwrite_range option must be a single boolean value");
}
if (overwrite_range_bool_val.IsNull()) {
throw BinderException("overwrite_range option must be a single boolean value");
}
overwrite_range = BooleanValue::Get(overwrite_range_bool_val);
} else {
overwrite_range = false;
}

const auto header_opt = options.find("header");
bool header;
if (header_opt != options.end()) {
if (header_opt->second.size() != 1) {
throw BinderException("header option must be a single boolean value");
}
string error_msg;
Value header_bool_val;
if (!header_opt->second.back().DefaultTryCastAs(LogicalType::BOOLEAN, header_bool_val, &error_msg)) {
throw BinderException("header option must be a single boolean value");
}
if (header_bool_val.IsNull()) {
throw BinderException("header option must be a single boolean value");
}
header = BooleanValue::Get(header_bool_val);
} else {
header = true;
// If we are in the append case, default to no header instead.
if (!overwrite_sheet && !overwrite_range) {
header = false;
}
if (create_if_not_exists && sheet.empty()) {
throw BinderException("Must provide sheet name");
}

return make_uniq<GSheetWriteBindData>(file_path, sql_types, names, sheet, range, overwrite_sheet, overwrite_range,
header);
create_if_not_exists, header);
}

unique_ptr<GlobalFunctionData> GSheetCopyFunction::GSheetWriteInitializeGlobal(ClientContext &context,
Expand Down Expand Up @@ -154,6 +114,16 @@ unique_ptr<GlobalFunctionData> GSheetCopyFunction::GSheetWriteInitializeGlobal(C
sheet_name = sheet.properties.title;
}

// Create sheet if not exist (if enabled)
if (options.create_if_not_exists) {
try {
auto sheet = client.Spreadsheets(spreadsheet_id).GetSheetByName(sheet_name);
// sheet already exists so we need to ignore this instruction from the user
} catch (sheets::SheetNotFoundException &_) {
client.Spreadsheets(spreadsheet_id).CreateSheet(sheet_name);
}
}

if (!options.range.empty()) {
sheet_range = options.range;
} else {
Expand Down
5 changes: 4 additions & 1 deletion src/include/gsheets_copy.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct GSheetWriteOptions {
std::string range;
bool overwrite_sheet;
bool overwrite_range;
bool create_if_not_exists;
bool header;
};

Expand All @@ -38,14 +39,16 @@ struct GSheetWriteBindData : public TableFunctionData {
vector<LogicalType> sql_types;

GSheetWriteBindData(string file_path, vector<LogicalType> sql_types, vector<string> names, std::string sheet,
std::string range, bool overwrite_sheet, bool overwrite_range, bool header)
std::string range, bool overwrite_sheet, bool overwrite_range, bool create_if_not_exists,
bool header)
: sql_types(std::move(sql_types)) {
files.push_back(std::move(file_path));
options.name_list = std::move(names);
options.sheet = std::move(sheet);
options.range = std::move(range);
options.overwrite_sheet = overwrite_sheet;
options.overwrite_range = overwrite_range;
options.create_if_not_exists = create_if_not_exists;
options.header = header;
}
};
Expand Down
9 changes: 7 additions & 2 deletions src/include/sheets/exception.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ class SheetsApiException : public SheetsException {
public:
SheetsApiException(int statusCode, const std::string &apiMessage)
: SheetsException("Google Sheets API error (" + std::to_string(statusCode) + "): " + apiMessage),
statusCode(statusCode),
apiMessage(apiMessage) {
statusCode(statusCode), apiMessage(apiMessage) {
}

int GetStatusCode() const {
Expand Down Expand Up @@ -53,5 +52,11 @@ class SheetNotFoundException : public SheetsException {
std::string identifier;
};

class SheetNotCreatedException : public SheetsException {
public:
explicit SheetNotCreatedException(const std::string &name) : SheetsException("Sheet not created: " + name) {
}
};

} // namespace sheets
} // namespace duckdb
8 changes: 6 additions & 2 deletions src/include/sheets/resources/spreadsheet.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@ class SpreadsheetResource : protected BaseResource {

SpreadsheetMetadata Get();

SheetMetadata GetSheetById(int sheetId);
SheetMetadata GetSheetById(const int sheetId);
SheetMetadata GetSheetById(const std::string &sheetId);
SheetMetadata GetSheetByName(const std::string &name);
SheetMetadata GetSheetByIndex(int index);
SheetMetadata GetSheetByIndex(const int index);

SheetMetadata CreateSheet(const std::string &name);

ValuesResource Values();

private:
std::string spreadsheetId;

SpreadsheetBatchUpdateResponse BatchUpdate(const SpreadsheetBatchUpdateRequest &req);
};

} // namespace sheets
Expand Down
36 changes: 36 additions & 0 deletions src/include/sheets/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,42 @@ struct SpreadsheetMetadata {

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SpreadsheetMetadata, spreadsheetId, properties, sheets)

struct AddSheetRequestProperties {
std::string title = "";
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(AddSheetRequestProperties, title)

struct AddSheetRequest {
AddSheetRequestProperties properties = {};
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(AddSheetRequest, properties)

struct SpreadsheetUpdateRequest {
AddSheetRequest addSheet = {};
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SpreadsheetUpdateRequest, addSheet);

struct SpreadsheetUpdateResponse {
SheetMetadata addSheet = {};
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SpreadsheetUpdateResponse, addSheet);

struct SpreadsheetBatchUpdateRequest {
std::vector<SpreadsheetUpdateRequest> requests = {};
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SpreadsheetBatchUpdateRequest, requests);

struct SpreadsheetBatchUpdateResponse {
std::vector<SpreadsheetUpdateResponse> replies = {};
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SpreadsheetBatchUpdateResponse, replies);

enum MajorDimension { DIMENSION_UNSPECIFIED, ROWS, COLUMNS };

NLOHMANN_JSON_SERIALIZE_ENUM(MajorDimension, {
Expand Down
30 changes: 28 additions & 2 deletions src/sheets/resources/spreadsheet.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
#include <string>

#include "json.hpp"

#include "sheets/resources/spreadsheet.hpp"
#include "sheets/exception.hpp"
#include "sheets/resources/values.hpp"
#include "sheets/types.hpp"
#include "sheets/util/response.hpp"

using json = nlohmann::json;

namespace duckdb {
namespace sheets {

Expand All @@ -13,7 +18,7 @@ SpreadsheetMetadata SpreadsheetResource::Get() {
return ParseResponse<SpreadsheetMetadata>(DoGet(path));
}

SheetMetadata SpreadsheetResource::GetSheetById(int sheetId) {
SheetMetadata SpreadsheetResource::GetSheetById(const int sheetId) {
auto meta = Get();
for (const auto &sheet : meta.sheets) {
if (sheet.properties.sheetId == sheetId) {
Expand All @@ -38,7 +43,7 @@ SheetMetadata SpreadsheetResource::GetSheetByName(const std::string &name) {
throw SheetNotFoundException(name);
}

SheetMetadata SpreadsheetResource::GetSheetByIndex(int index) {
SheetMetadata SpreadsheetResource::GetSheetByIndex(const int index) {
auto meta = Get();
for (const auto &sheet : meta.sheets) {
if (sheet.properties.index == index) {
Expand All @@ -48,6 +53,27 @@ SheetMetadata SpreadsheetResource::GetSheetByIndex(int index) {
throw SheetNotFoundException(std::to_string(index));
}

SheetMetadata SpreadsheetResource::CreateSheet(const std::string &name) {
SpreadsheetUpdateRequest update;
update.addSheet.properties.title = name;

SpreadsheetBatchUpdateRequest req;
req.requests.push_back(update);

SpreadsheetBatchUpdateResponse res = BatchUpdate(req);
if (res.replies.empty()) {
throw SheetNotCreatedException(name);
}
auto reply = res.replies.front();
return reply.addSheet;
}

SpreadsheetBatchUpdateResponse SpreadsheetResource::BatchUpdate(const SpreadsheetBatchUpdateRequest &req) {
std::string path = "/spreadsheets/" + spreadsheetId + ":batchUpdate";
std::string body = json(req).dump();
return ParseResponse<SpreadsheetBatchUpdateResponse>(DoPost(path, body));
}

ValuesResource SpreadsheetResource::Values() {
return ValuesResource(http, headers, baseUrl, spreadsheetId);
}
Expand Down
Loading
Loading