Skip to content

Commit 3e5e818

Browse files
author
Michael Harris
authored
Merge pull request #103 from mharrisb1/95-is-it-possible-to-create-a-sheet-if-it-doesnt-exist
feat(copy): add flag to create sheet if not exists
2 parents 7be155e + 5e69a25 commit 3e5e818

10 files changed

Lines changed: 183 additions & 102 deletions

File tree

.clangd

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
If:
2+
PathMatch: src/.*
3+
4+
CompileFlags:
5+
CompilationDatabase: build/release
6+
7+
---
8+
If:
9+
PathMatch: test/.*
10+
11+
CompileFlags:
12+
CompilationDatabase: build/unit_tests

docs/pages/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ COPY <table_name> TO '11QdEasMWbETbFVxry-SsD8jVcdYIT1zBQszcF84MdE8' (FORMAT gshe
116116
-- Write a spreadsheet from a table by full URL
117117
COPY <table_name> TO 'https://docs.google.com/spreadsheets/d/11QdEasMWbETbFVxry-SsD8jVcdYIT1zBQszcF84MdE8/edit?usp=sharing' (FORMAT gsheet);
118118

119+
-- Create a sheet if it doesn't already exist
120+
COPY <table_name> TO 'https://docs.google.com/spreadsheets/d/11QdEasMWbETbFVxry-SsD8jVcdYIT1zBQszcF84MdE8/edit' (FORMAT gsheet, sheet 'Woot', create_if_not_exists true);
121+
119122
-- Write a spreadsheet to a specific sheet using the sheet id in the URL
120123
COPY <table_name> TO 'https://docs.google.com/spreadsheets/d/11QdEasMWbETbFVxry-SsD8jVcdYIT1zBQszcF84MdE8/edit?gid=1295634987#gid=1295634987' (FORMAT gsheet);
121124

src/gsheets_copy.cpp

Lines changed: 64 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
#include <vector>
1+
#include <utility>
22

3+
#include "duckdb/common/case_insensitive_map.hpp"
34
#include "duckdb/common/exception.hpp"
5+
#include "duckdb/common/exception/binder_exception.hpp"
46
#include "duckdb/common/file_system.hpp"
7+
#include "duckdb/common/types.hpp"
58
#include "duckdb/common/types/value.hpp"
69

710
#include "gsheets_copy.hpp"
811
#include "gsheets_utils.hpp"
912

1013
#include "sheets/auth_factory.hpp"
1114
#include "sheets/client.hpp"
15+
#include "sheets/exception.hpp"
1216
#include "sheets/range.hpp"
1317
#include "sheets/transport/client_factory.hpp"
1418
#include "sheets/types.hpp"
@@ -22,110 +26,66 @@ GSheetCopyFunction::GSheetCopyFunction() : CopyFunction("gsheet") {
2226
copy_to_sink = GSheetWriteSink;
2327
}
2428

29+
static std::string GetStringOption(const case_insensitive_map_t<vector<Value>> &options, const std::string &name,
30+
const std::string &default_value = "") {
31+
const auto it = options.find(name);
32+
if (it == options.end()) {
33+
return default_value;
34+
}
35+
std::string err;
36+
Value val;
37+
if (!it->second.back().DefaultTryCastAs(LogicalType::VARCHAR, val, &err)) {
38+
throw BinderException(name + " option must be VARCHAR");
39+
}
40+
if (val.IsNull()) {
41+
throw BinderException(name + " option must not be NULL");
42+
}
43+
return StringValue::Get(val);
44+
}
45+
46+
// NOTE: the second value in pair is a flag indicating if the value was set by the user
47+
static std::pair<bool, bool> GetBoolOption(const case_insensitive_map_t<vector<Value>> &options,
48+
const std::string &name, bool default_value = false) {
49+
const auto it = options.find(name);
50+
if (it == options.end()) {
51+
return std::make_pair(default_value, false);
52+
}
53+
if (it->second.size() != 1) {
54+
throw BinderException(name + " option must be a single boolean value");
55+
}
56+
std::string err;
57+
Value val;
58+
if (!it->second.back().DefaultTryCastAs(LogicalType::BOOLEAN, val, &err)) {
59+
throw BinderException(name + " option must be a single boolean value");
60+
}
61+
if (val.IsNull()) {
62+
throw BinderException(name + " option must be a single boolean value");
63+
}
64+
return std::make_pair(BooleanValue::Get(val), true);
65+
}
66+
2567
unique_ptr<FunctionData> GSheetCopyFunction::GSheetWriteBind(ClientContext &context, CopyFunctionBindInput &input,
2668
const vector<string> &names,
2769
const vector<LogicalType> &sql_types) {
28-
string file_path = input.info.file_path;
2970

71+
string file_path = input.info.file_path;
3072
auto options = input.info.options;
3173

32-
const auto sheet_opt = options.find("sheet");
33-
std::string sheet;
34-
if (sheet_opt != options.end()) {
35-
string error_msg;
36-
Value sheet_val;
37-
if (!sheet_opt->second.back().DefaultTryCastAs(LogicalType::VARCHAR, sheet_val, &error_msg)) {
38-
throw BinderException("sheet option must be a VARCHAR");
39-
}
40-
if (sheet_val.IsNull()) {
41-
throw BinderException("sheet option must be a non-null VARCHAR");
42-
}
43-
sheet = StringValue::Get(sheet_val);
44-
} else {
45-
sheet = "";
46-
}
74+
auto sheet = GetStringOption(options, "sheet");
75+
auto range = GetStringOption(options, "range");
76+
bool overwrite_sheet = GetBoolOption(options, "overwrite_sheet", true).first;
77+
bool overwrite_range = GetBoolOption(options, "overwrite_range", false).first;
78+
bool create_if_not_exists = GetBoolOption(options, "create_if_not_exists", false).first;
4779

48-
const auto range_opt = options.find("range");
49-
std::string range;
50-
if (range_opt != options.end()) {
51-
string error_msg;
52-
Value range_val;
53-
if (!range_opt->second.back().DefaultTryCastAs(LogicalType::VARCHAR, range_val, &error_msg)) {
54-
throw BinderException("range option must be a VARCHAR");
55-
}
56-
if (range_val.IsNull()) {
57-
throw BinderException("range option must be a non-null VARCHAR");
58-
}
59-
range = StringValue::Get(range_val);
60-
} else {
61-
range = "";
62-
}
80+
auto header_result = GetBoolOption(options, "header", true);
81+
bool header = header_result.second ? header_result.first : (overwrite_range || overwrite_sheet);
6382

64-
const auto overwrite_sheet_opt = options.find("overwrite_sheet");
65-
bool overwrite_sheet;
66-
if (overwrite_sheet_opt != options.end()) {
67-
if (overwrite_sheet_opt->second.size() != 1) {
68-
throw BinderException("overwrite_sheet option must be a single boolean value");
69-
}
70-
string error_msg;
71-
Value overwrite_sheet_bool_val;
72-
if (!overwrite_sheet_opt->second.back().DefaultTryCastAs(LogicalType::BOOLEAN, overwrite_sheet_bool_val,
73-
&error_msg)) {
74-
throw BinderException("overwrite_sheet option must be a single boolean value");
75-
}
76-
if (overwrite_sheet_bool_val.IsNull()) {
77-
throw BinderException("overwrite_sheet option must be a single boolean value");
78-
}
79-
overwrite_sheet = BooleanValue::Get(overwrite_sheet_bool_val);
80-
} else {
81-
overwrite_sheet = true; // Default to overwrite_sheet to maintain prior behavior
82-
}
83-
84-
const auto overwrite_range_opt = options.find("overwrite_range");
85-
bool overwrite_range;
86-
if (overwrite_range_opt != options.end()) {
87-
if (overwrite_range_opt->second.size() != 1) {
88-
throw BinderException("overwrite_range option must be a single boolean value");
89-
}
90-
string error_msg;
91-
Value overwrite_range_bool_val;
92-
if (!overwrite_range_opt->second.back().DefaultTryCastAs(LogicalType::BOOLEAN, overwrite_range_bool_val,
93-
&error_msg)) {
94-
throw BinderException("overwrite_range option must be a single boolean value");
95-
}
96-
if (overwrite_range_bool_val.IsNull()) {
97-
throw BinderException("overwrite_range option must be a single boolean value");
98-
}
99-
overwrite_range = BooleanValue::Get(overwrite_range_bool_val);
100-
} else {
101-
overwrite_range = false;
102-
}
103-
104-
const auto header_opt = options.find("header");
105-
bool header;
106-
if (header_opt != options.end()) {
107-
if (header_opt->second.size() != 1) {
108-
throw BinderException("header option must be a single boolean value");
109-
}
110-
string error_msg;
111-
Value header_bool_val;
112-
if (!header_opt->second.back().DefaultTryCastAs(LogicalType::BOOLEAN, header_bool_val, &error_msg)) {
113-
throw BinderException("header option must be a single boolean value");
114-
}
115-
if (header_bool_val.IsNull()) {
116-
throw BinderException("header option must be a single boolean value");
117-
}
118-
header = BooleanValue::Get(header_bool_val);
119-
} else {
120-
header = true;
121-
// If we are in the append case, default to no header instead.
122-
if (!overwrite_sheet && !overwrite_range) {
123-
header = false;
124-
}
83+
if (create_if_not_exists && sheet.empty()) {
84+
throw BinderException("Must provide sheet name");
12585
}
12686

12787
return make_uniq<GSheetWriteBindData>(file_path, sql_types, names, sheet, range, overwrite_sheet, overwrite_range,
128-
header);
88+
create_if_not_exists, header);
12989
}
13090

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

117+
// Create sheet if not exist (if enabled)
118+
if (options.create_if_not_exists) {
119+
try {
120+
auto sheet = client.Spreadsheets(spreadsheet_id).GetSheetByName(sheet_name);
121+
// sheet already exists so we need to ignore this instruction from the user
122+
} catch (sheets::SheetNotFoundException &_) {
123+
client.Spreadsheets(spreadsheet_id).CreateSheet(sheet_name);
124+
}
125+
}
126+
157127
if (!options.range.empty()) {
158128
sheet_range = options.range;
159129
} else {

src/include/gsheets_copy.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ struct GSheetWriteOptions {
2929
std::string range;
3030
bool overwrite_sheet;
3131
bool overwrite_range;
32+
bool create_if_not_exists;
3233
bool header;
3334
};
3435

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

4041
GSheetWriteBindData(string file_path, vector<LogicalType> sql_types, vector<string> names, std::string sheet,
41-
std::string range, bool overwrite_sheet, bool overwrite_range, bool header)
42+
std::string range, bool overwrite_sheet, bool overwrite_range, bool create_if_not_exists,
43+
bool header)
4244
: sql_types(std::move(sql_types)) {
4345
files.push_back(std::move(file_path));
4446
options.name_list = std::move(names);
4547
options.sheet = std::move(sheet);
4648
options.range = std::move(range);
4749
options.overwrite_sheet = overwrite_sheet;
4850
options.overwrite_range = overwrite_range;
51+
options.create_if_not_exists = create_if_not_exists;
4952
options.header = header;
5053
}
5154
};

src/include/sheets/exception.hpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ class SheetsApiException : public SheetsException {
1616
public:
1717
SheetsApiException(int statusCode, const std::string &apiMessage)
1818
: SheetsException("Google Sheets API error (" + std::to_string(statusCode) + "): " + apiMessage),
19-
statusCode(statusCode),
20-
apiMessage(apiMessage) {
19+
statusCode(statusCode), apiMessage(apiMessage) {
2120
}
2221

2322
int GetStatusCode() const {
@@ -53,5 +52,11 @@ class SheetNotFoundException : public SheetsException {
5352
std::string identifier;
5453
};
5554

55+
class SheetNotCreatedException : public SheetsException {
56+
public:
57+
explicit SheetNotCreatedException(const std::string &name) : SheetsException("Sheet not created: " + name) {
58+
}
59+
};
60+
5661
} // namespace sheets
5762
} // namespace duckdb

src/include/sheets/resources/spreadsheet.hpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,19 @@ class SpreadsheetResource : protected BaseResource {
1515

1616
SpreadsheetMetadata Get();
1717

18-
SheetMetadata GetSheetById(int sheetId);
18+
SheetMetadata GetSheetById(const int sheetId);
1919
SheetMetadata GetSheetById(const std::string &sheetId);
2020
SheetMetadata GetSheetByName(const std::string &name);
21-
SheetMetadata GetSheetByIndex(int index);
21+
SheetMetadata GetSheetByIndex(const int index);
22+
23+
SheetMetadata CreateSheet(const std::string &name);
2224

2325
ValuesResource Values();
2426

2527
private:
2628
std::string spreadsheetId;
29+
30+
SpreadsheetBatchUpdateResponse BatchUpdate(const SpreadsheetBatchUpdateRequest &req);
2731
};
2832

2933
} // namespace sheets

src/include/sheets/types.hpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,42 @@ struct SpreadsheetMetadata {
4848

4949
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SpreadsheetMetadata, spreadsheetId, properties, sheets)
5050

51+
struct AddSheetRequestProperties {
52+
std::string title = "";
53+
};
54+
55+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(AddSheetRequestProperties, title)
56+
57+
struct AddSheetRequest {
58+
AddSheetRequestProperties properties = {};
59+
};
60+
61+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(AddSheetRequest, properties)
62+
63+
struct SpreadsheetUpdateRequest {
64+
AddSheetRequest addSheet = {};
65+
};
66+
67+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SpreadsheetUpdateRequest, addSheet);
68+
69+
struct SpreadsheetUpdateResponse {
70+
SheetMetadata addSheet = {};
71+
};
72+
73+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SpreadsheetUpdateResponse, addSheet);
74+
75+
struct SpreadsheetBatchUpdateRequest {
76+
std::vector<SpreadsheetUpdateRequest> requests = {};
77+
};
78+
79+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SpreadsheetBatchUpdateRequest, requests);
80+
81+
struct SpreadsheetBatchUpdateResponse {
82+
std::vector<SpreadsheetUpdateResponse> replies = {};
83+
};
84+
85+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SpreadsheetBatchUpdateResponse, replies);
86+
5187
enum MajorDimension { DIMENSION_UNSPECIFIED, ROWS, COLUMNS };
5288

5389
NLOHMANN_JSON_SERIALIZE_ENUM(MajorDimension, {

src/sheets/resources/spreadsheet.cpp

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
#include <string>
22

3+
#include "json.hpp"
4+
35
#include "sheets/resources/spreadsheet.hpp"
46
#include "sheets/exception.hpp"
7+
#include "sheets/resources/values.hpp"
58
#include "sheets/types.hpp"
69
#include "sheets/util/response.hpp"
710

11+
using json = nlohmann::json;
12+
813
namespace duckdb {
914
namespace sheets {
1015

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

16-
SheetMetadata SpreadsheetResource::GetSheetById(int sheetId) {
21+
SheetMetadata SpreadsheetResource::GetSheetById(const int sheetId) {
1722
auto meta = Get();
1823
for (const auto &sheet : meta.sheets) {
1924
if (sheet.properties.sheetId == sheetId) {
@@ -38,7 +43,7 @@ SheetMetadata SpreadsheetResource::GetSheetByName(const std::string &name) {
3843
throw SheetNotFoundException(name);
3944
}
4045

41-
SheetMetadata SpreadsheetResource::GetSheetByIndex(int index) {
46+
SheetMetadata SpreadsheetResource::GetSheetByIndex(const int index) {
4247
auto meta = Get();
4348
for (const auto &sheet : meta.sheets) {
4449
if (sheet.properties.index == index) {
@@ -48,6 +53,27 @@ SheetMetadata SpreadsheetResource::GetSheetByIndex(int index) {
4853
throw SheetNotFoundException(std::to_string(index));
4954
}
5055

56+
SheetMetadata SpreadsheetResource::CreateSheet(const std::string &name) {
57+
SpreadsheetUpdateRequest update;
58+
update.addSheet.properties.title = name;
59+
60+
SpreadsheetBatchUpdateRequest req;
61+
req.requests.push_back(update);
62+
63+
SpreadsheetBatchUpdateResponse res = BatchUpdate(req);
64+
if (res.replies.empty()) {
65+
throw SheetNotCreatedException(name);
66+
}
67+
auto reply = res.replies.front();
68+
return reply.addSheet;
69+
}
70+
71+
SpreadsheetBatchUpdateResponse SpreadsheetResource::BatchUpdate(const SpreadsheetBatchUpdateRequest &req) {
72+
std::string path = "/spreadsheets/" + spreadsheetId + ":batchUpdate";
73+
std::string body = json(req).dump();
74+
return ParseResponse<SpreadsheetBatchUpdateResponse>(DoPost(path, body));
75+
}
76+
5177
ValuesResource SpreadsheetResource::Values() {
5278
return ValuesResource(http, headers, baseUrl, spreadsheetId);
5379
}

0 commit comments

Comments
 (0)