Skip to content

Commit 3e39f76

Browse files
authored
fix: correct Microsoft Graph docs and add column-name delete (#44)
Reconciles the Microsoft Graph README sections with the registered functions (resolves #43), adds a column-name overload + site param to graph_excel_delete_rows, and renames graph_team_members to graph_teams_members for consistency.
1 parent 31af509 commit 3e39f76

9 files changed

Lines changed: 229 additions & 71 deletions

README.md

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -714,10 +714,10 @@ SELECT id, display_name, operating_system, os_version, trust_type
714714
FROM graph_devices();
715715

716716
-- Sign-in audit log (Azure AD Premium required)
717-
SELECT user_display_name, app_display_name, ip_address, status, created_at
717+
SELECT user_display_name, app_display_name, ip_address, status, created_datetime
718718
FROM graph_signin_logs()
719719
WHERE status != 'Success'
720-
ORDER BY created_at DESC;
720+
ORDER BY created_datetime DESC;
721721
```
722722

723723
Functions: `graph_users([secret])`, `graph_groups([secret])`, `graph_devices([secret])`, `graph_signin_logs([secret])`
@@ -801,56 +801,52 @@ SELECT id, name, drive_type, web_url
801801
FROM graph_show_drives(site := 'Finance', secret := 'ms_graph');
802802

803803
-- Lists in a site (by name or ID)
804-
SELECT id, name, display_name, item_count
804+
SELECT id, name, display_name, description
805805
FROM graph_show_lists(site := 'Finance', secret := 'ms_graph');
806806

807807
-- Schema of a list (site and list accept names, URLs, or GUIDs)
808-
SELECT column_name, column_type, required
808+
SELECT name, column_type, required
809809
FROM graph_describe_list('Finance', 'Budget');
810810

811811
-- Items in a list — filter pushdown reduces server-side payload
812-
SELECT * FROM graph_list_items('Finance', 'Budget', secret := 'ms_graph')
812+
SELECT * FROM graph_sharepoint_list_read('Finance', 'Budget', secret := 'ms_graph')
813813
WHERE status = 'Active';
814814

815815
-- Works equally with site URLs and list display names
816-
SELECT * FROM graph_list_items(
816+
SELECT * FROM graph_sharepoint_list_read(
817817
'https://tenant.sharepoint.com/sites/Finance',
818818
'Project Tracker',
819819
secret := 'ms_graph'
820820
);
821821
```
822822

823-
Functions: `graph_show_sites([secret])`, `graph_show_drives([site_id], [secret, site])`, `graph_show_lists([site_id], [secret, site])`, `graph_describe_list(site_id_or_name, list_id_or_name, [secret])`, `graph_list_items(site_id_or_name, list_id_or_name, [secret])`
823+
Functions: `graph_show_sites([secret])`, `graph_show_drives([site_id], [secret, site])`, `graph_show_lists([site_id], [secret, site])`, `graph_describe_list(site_id_or_name, list_id_or_name, [secret])`, `graph_sharepoint_list_read(site_id_or_name, list_id_or_name, [secret])`
824824

825825
#### Writing list items
826826

827827
Required permissions: `Sites.ReadWrite.All` (Application).
828828

829-
`graph_sharepoint_create_item`, `graph_sharepoint_update_item`, and `graph_sharepoint_delete_item` are table functions — use them in `SELECT` or with a lateral join for per-row mutations.
829+
`graph_sharepoint_create_item` is a **table function** returning the new `item_id`; use it in `SELECT` or with a `LATERAL` join. `graph_sharepoint_update_item` and `graph_sharepoint_delete_item` are **scalar functions** returning `BOOLEAN` (true on success) — note their `secret` is the **last positional argument**, not a named parameter.
830830

831831
```sql
832-
-- Create a new item (fields as JSON object)
833-
SELECT item_id, item_url
832+
-- Create a new item (fields as JSON object) — returns the new item_id
833+
SELECT item_id
834834
FROM graph_sharepoint_create_item(
835835
'Finance',
836836
'Budget',
837837
'{"Title": "New Entry", "Status": "Draft", "Amount": 1500}',
838838
secret := 'ms_graph'
839839
);
840840

841-
-- Update an existing item by ID
842-
SELECT item_id, item_url
843-
FROM graph_sharepoint_update_item(
844-
'Finance',
845-
'Budget',
846-
'item-id-here',
841+
-- Update an existing item by ID — scalar, returns true on success (secret is positional)
842+
SELECT graph_sharepoint_update_item(
843+
'Finance', 'Budget', 'item-id-here',
847844
'{"Status": "Approved"}',
848-
secret := 'ms_graph'
849-
);
845+
'ms_graph'
846+
) AS updated;
850847

851-
-- Delete an item by ID
852-
SELECT item_id
853-
FROM graph_sharepoint_delete_item('Finance', 'Budget', 'item-id-here', secret := 'ms_graph');
848+
-- Delete an item by ID — scalar, returns true on success (secret is positional)
849+
SELECT graph_sharepoint_delete_item('Finance', 'Budget', 'item-id-here', 'ms_graph') AS deleted;
854850

855851
-- Bulk-create items from a query result
856852
SELECT src.title, p.item_id
@@ -895,27 +891,27 @@ File paths and drive locations accept either a **friendly name** (`site := 'Fina
895891

896892
```sql
897893
-- Files accessible in OneDrive/SharePoint (by name or raw drive_id)
898-
SELECT name, file_path, size, last_modified
899-
FROM graph_list_files(site := 'Finance', drive := 'Documents', secret := 'ms_graph');
894+
SELECT id, name, web_url, size, created_at, modified_at, mime_type, is_folder
895+
FROM graph_show_files(site := 'Finance', drive := 'Documents', secret := 'ms_graph');
900896

901897
-- Worksheets in a workbook
902-
SELECT id, name, position
898+
SELECT name, id, position, visibility
903899
FROM graph_excel_worksheets('Budget.xlsx',
904900
site := 'Finance',
905901
drive := 'Documents',
906902
secret := 'ms_graph'
907903
);
908904

909905
-- Named tables in a workbook
910-
SELECT id, name, row_count
906+
SELECT name, id, show_headers, show_totals
911907
FROM graph_excel_tables('Budget.xlsx',
912908
site := 'Finance',
913909
drive := 'Documents',
914910
secret := 'ms_graph'
915911
);
916912

917913
-- Read table data
918-
SELECT * FROM graph_excel_table_data('Budget.xlsx', 'SalesData',
914+
SELECT * FROM graph_excel_read('Budget.xlsx', 'SalesData',
919915
site := 'Finance',
920916
drive := 'Documents',
921917
secret := 'ms_graph'
@@ -929,7 +925,7 @@ SELECT * FROM graph_excel_range('Budget.xlsx', 'Sheet1',
929925
);
930926
```
931927

932-
Functions: `graph_list_files([secret, drive_id, site, drive])`, `graph_excel_worksheets(file_path, [secret, drive_id, site, drive])`, `graph_excel_tables(file_path, [secret, drive_id, site, drive])`, `graph_excel_table_data(file_path, table_name, [secret, drive_id, site, drive])`, `graph_excel_range(file_path, sheet_name, [secret, drive_id, site, drive])`
928+
Functions: `graph_show_files([folder_path], [secret, drive_id, site, drive])`, `graph_excel_worksheets(file_path, [secret, drive_id, site, drive])`, `graph_excel_tables(file_path, [secret, drive_id, site, drive])`, `graph_excel_read(file_path, table_name, [secret, drive_id, site, drive])`, `graph_excel_range(file_path, sheet_name, [secret, drive_id, site, drive])`
933929

934930
#### Writing Excel data
935931

@@ -958,17 +954,17 @@ The `drive` option scopes the path to a specific SharePoint drive: `(TYPE excel_
958954
**Delete rows by column value:**
959955

960956
```sql
961-
-- Delete all rows where a named column equals a given value
957+
-- Delete all rows where a column equals a given value (column by name or 0-based index)
962958
SELECT rows_deleted
963959
FROM graph_excel_delete_rows(
964960
'Budget.xlsx', 'SalesData',
965-
'Region', 'North', -- column name and match value
961+
'Region', 'North', -- column name (or a 0-based index like 0) and match value
966962
site := 'Finance',
967963
secret := 'ms_graph'
968964
);
969965
```
970966

971-
Functions: `graph_excel_delete_rows(file_path, table_name, col_name, col_value, [secret, drive_id, site, drive])`
967+
Functions: `graph_excel_delete_rows(file_path, table_name, column, col_value, [secret, drive, site])``column` is a column name (resolved against the table header) or a 0-based index; `col_value` is always compared as a string.
972968

973969
Storage extension: `ATTACH '<file-path>' AS <catalog> (TYPE excel_workbook, SECRET '<secret>'[, drive '<drive-id>'])`
974970

@@ -1015,18 +1011,18 @@ FROM graph_contacts(user := 'user-guid-or-upn', secret := 'ms_graph');
10151011

10161012
-- Discover mail folders
10171013
SELECT display_name, total_item_count, unread_item_count
1018-
FROM graph_mail_folders(user := 'user-guid-or-upn', secret := 'ms_graph');
1014+
FROM graph_outlook_mail_folders(user := 'user-guid-or-upn', secret := 'ms_graph');
10191015

10201016
-- Email messages — metadata only, no body content
10211017
SELECT subject, from_name, from_email, received_at, importance, is_read
1022-
FROM graph_messages(user := 'user-guid-or-upn', folder := 'inbox', secret := 'ms_graph')
1018+
FROM graph_outlook_emails(user := 'user-guid-or-upn', folder := 'inbox', secret := 'ms_graph')
10231019
ORDER BY received_at DESC;
10241020

1025-
-- folder accepts well-known names or any display name from graph_mail_folders()
1021+
-- folder accepts well-known names or any display name from graph_outlook_mail_folders()
10261022
-- well-known: inbox, sentitems, drafts, deleteditems, junkemail, outbox, archive
10271023
```
10281024

1029-
Functions: `graph_calendars([user, secret])`, `graph_calendar_events([user, calendar_id, start_date, end_date, secret])`, `graph_contacts([user, secret])`, `graph_mail_folders([user, secret])`, `graph_messages([user, folder, secret])`
1025+
Functions: `graph_calendars([user, secret])`, `graph_calendar_events([user, calendar_id, start_date, end_date, secret])`, `graph_contacts([user, secret])`, `graph_outlook_mail_folders([user, secret])`, `graph_outlook_emails([user, folder, secret])`
10301026

10311027
`start_date` and `end_date` must be provided together; bare ISO dates (`'2024-01-01'`) are accepted alongside full datetimes.
10321028

@@ -1043,19 +1039,19 @@ FROM graph_my_teams();
10431039

10441040
-- Channels in a team
10451041
SELECT id, display_name, membership_type
1046-
FROM graph_team_channels('your-team-id');
1042+
FROM graph_teams_channels('your-team-id');
10471043

10481044
-- Members of a team
1049-
SELECT id, display_name, roles
1050-
FROM graph_team_members('your-team-id');
1045+
SELECT id, display_name, role
1046+
FROM graph_teams_members('your-team-id');
10511047

10521048
-- Messages in a channel
1053-
SELECT id, subject, body, created_at, from_user
1049+
SELECT id, created_datetime, from_name, body_content, importance
10541050
FROM graph_channel_messages('your-team-id', 'your-channel-id')
1055-
ORDER BY created_at DESC;
1051+
ORDER BY created_datetime DESC;
10561052
```
10571053

1058-
Functions: `graph_my_teams([user, secret])`, `graph_team_channels(team_id, [secret])`, `graph_team_members(team_id, [secret])`, `graph_channel_messages(team_id, channel_id, [secret])`
1054+
Functions: `graph_my_teams([user, secret])`, `graph_teams_channels(team_id, [secret])`, `graph_teams_members(team_id, [secret])`, `graph_channel_messages(team_id, channel_id, [secret])`
10591055

10601056
`user` accepts a GUID, UPN, or email. Omit it for delegated (`/me/joinedTeams`); provide it for app-only auth (`/users/{id}/joinedTeams`).
10611057

src/graph_excel_functions.cpp

Lines changed: 103 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "graph_output_utils.hpp"
88
#include "tracing.hpp"
99
#include "duckdb/common/exception.hpp"
10+
#include "duckdb/common/string_util.hpp"
1011
#include "duckdb/common/types/date.hpp"
1112
#include "duckdb/parser/parsed_data/create_table_function_info.hpp"
1213
#include "yyjson.hpp"
@@ -870,6 +871,8 @@ struct DeleteRowsBindData : public TableFunctionData {
870871
std::string col_value;
871872
std::string drive_id;
872873
std::string secret_name;
874+
bool by_name = false; // when true, col_name is resolved to col_index at scan time
875+
std::string col_name;
873876
bool done = false;
874877
};
875878

@@ -898,6 +901,62 @@ unique_ptr<FunctionData> GraphExcelFunctions::DeleteRowsBind(
898901
return std::move(bind);
899902
}
900903

904+
unique_ptr<FunctionData> GraphExcelFunctions::DeleteRowsByNameBind(
905+
ClientContext &context,
906+
TableFunctionBindInput &input,
907+
vector<LogicalType> &return_types,
908+
vector<std::string> &names) {
909+
910+
if (input.inputs.size() < 4) {
911+
throw BinderException("graph_excel_delete_rows requires file_path, table_name, column, col_value");
912+
}
913+
auto bind = make_uniq<DeleteRowsBindData>();
914+
bind->file_path = input.inputs[0].GetValue<std::string>();
915+
bind->table_name = input.inputs[1].GetValue<std::string>();
916+
bind->by_name = true;
917+
bind->col_name = input.inputs[2].GetValue<std::string>();
918+
bind->col_value = input.inputs[3].GetValue<std::string>();
919+
920+
if (input.named_parameters.count("secret")) {
921+
bind->secret_name = input.named_parameters.at("secret").GetValue<std::string>();
922+
}
923+
bind->drive_id = ResolveGraphDriveId(context, bind->secret_name, input);
924+
925+
return_types = {LogicalType::BIGINT};
926+
names = {"rows_deleted"};
927+
return std::move(bind);
928+
}
929+
930+
idx_t GraphExcelFunctions::ResolveColumnIndex(const std::vector<std::string> &columns,
931+
const std::string &column_ref) {
932+
// Exact name match first.
933+
for (idx_t i = 0; i < columns.size(); i++) {
934+
if (columns[i] == column_ref) {
935+
return i;
936+
}
937+
}
938+
// Case-insensitive name match.
939+
const std::string lowered_ref = StringUtil::Lower(column_ref);
940+
for (idx_t i = 0; i < columns.size(); i++) {
941+
if (StringUtil::Lower(columns[i]) == lowered_ref) {
942+
return i;
943+
}
944+
}
945+
// Fallback: a purely numeric reference is treated as a 0-based index.
946+
if (!column_ref.empty() &&
947+
column_ref.find_first_not_of("0123456789") == std::string::npos) {
948+
return static_cast<idx_t>(std::stoull(column_ref));
949+
}
950+
std::string available;
951+
for (idx_t i = 0; i < columns.size(); i++) {
952+
if (i > 0) { available += ", "; }
953+
available += columns[i];
954+
}
955+
throw InvalidInputException(
956+
"graph_excel_delete_rows: column '%s' not found. Available columns: %s",
957+
column_ref, available);
958+
}
959+
901960
void GraphExcelFunctions::DeleteRowsScan(
902961
ClientContext &context,
903962
TableFunctionInput &data_p,
@@ -909,8 +968,17 @@ void GraphExcelFunctions::DeleteRowsScan(
909968

910969
auto auth_info = ResolveGraphAuth(context, bind.secret_name);
911970
GraphExcelClient client(auth_info.auth_params);
971+
972+
idx_t col_index = bind.col_index;
973+
if (bind.by_name) {
974+
const auto columns = client.GetTableColumnsByPath(bind.file_path, bind.table_name, bind.drive_id);
975+
col_index = ResolveColumnIndex(columns, bind.col_name);
976+
ERPL_TRACE_DEBUG("GRAPH_EXCEL",
977+
"Resolved delete column '" + bind.col_name + "' to index " + std::to_string(col_index));
978+
}
979+
912980
const idx_t n = client.DeleteTableRowsMatchingColumn(
913-
bind.file_path, bind.table_name, bind.col_index, bind.col_value, bind.drive_id);
981+
bind.file_path, bind.table_name, col_index, bind.col_value, bind.drive_id);
914982
output.SetCardinality(1);
915983
output.SetValue(0, 0, Value::BIGINT(static_cast<int64_t>(n)));
916984
}
@@ -1076,29 +1144,48 @@ void GraphExcelFunctions::Register(ExtensionLoader &loader) {
10761144
loader.RegisterFunction(std::move(info));
10771145
}
10781146

1079-
// graph_excel_delete_rows: delete rows matching a column value
1147+
// graph_excel_delete_rows: delete rows matching a column value. Two overloads share the same
1148+
// name: the column may be given as a 0-based index (BIGINT) or as a column name (VARCHAR),
1149+
// which is resolved against the table header at scan time.
10801150
{
1081-
TableFunction del_rows("graph_excel_delete_rows",
1082-
{LogicalType::VARCHAR, LogicalType::VARCHAR,
1083-
LogicalType::BIGINT, LogicalType::VARCHAR},
1084-
GraphExcelFunctions::DeleteRowsScan,
1085-
GraphExcelFunctions::DeleteRowsBind);
1086-
del_rows.named_parameters["drive"] = LogicalType::VARCHAR;
1087-
del_rows.named_parameters["secret"] = LogicalType::VARCHAR;
1088-
1089-
CreateTableFunctionInfo info(del_rows);
1151+
const auto add_named_params = [](TableFunction &fn) {
1152+
fn.named_parameters["drive"] = LogicalType::VARCHAR;
1153+
fn.named_parameters["secret"] = LogicalType::VARCHAR;
1154+
fn.named_parameters["site"] = LogicalType::VARCHAR;
1155+
};
1156+
1157+
TableFunctionSet del_rows_set("graph_excel_delete_rows");
1158+
1159+
TableFunction del_by_index("graph_excel_delete_rows",
1160+
{LogicalType::VARCHAR, LogicalType::VARCHAR,
1161+
LogicalType::BIGINT, LogicalType::VARCHAR},
1162+
GraphExcelFunctions::DeleteRowsScan,
1163+
GraphExcelFunctions::DeleteRowsBind);
1164+
add_named_params(del_by_index);
1165+
del_rows_set.AddFunction(del_by_index);
1166+
1167+
TableFunction del_by_name("graph_excel_delete_rows",
1168+
{LogicalType::VARCHAR, LogicalType::VARCHAR,
1169+
LogicalType::VARCHAR, LogicalType::VARCHAR},
1170+
GraphExcelFunctions::DeleteRowsScan,
1171+
GraphExcelFunctions::DeleteRowsByNameBind);
1172+
add_named_params(del_by_name);
1173+
del_rows_set.AddFunction(del_by_name);
1174+
1175+
CreateTableFunctionInfo info(del_rows_set);
10901176
FunctionDescription desc;
10911177
desc.description = "Delete all rows in an Excel table where a column value matches. "
10921178
"Returns rows_deleted. "
1093-
"col_index is 0-based (0 = first column). "
1179+
"The column may be given as a name (resolved against the table header) "
1180+
"or as a 0-based integer index. "
10941181
"col_value is always compared as a string; cast numeric IDs to VARCHAR if needed.";
1095-
desc.parameter_names = {"file_path", "table_name", "col_index", "col_value"};
1182+
desc.parameter_names = {"file_path", "table_name", "column", "col_value"};
10961183
desc.parameter_types = {LogicalType::VARCHAR, LogicalType::VARCHAR,
1097-
LogicalType::BIGINT, LogicalType::VARCHAR};
1184+
LogicalType::VARCHAR, LogicalType::VARCHAR};
10981185
desc.examples = {
1099-
"SELECT * FROM graph_excel_delete_rows('report.xlsx', 'Sales', 0, 'obsolete_row', "
1186+
"SELECT * FROM graph_excel_delete_rows('report.xlsx', 'Sales', 'Region', 'North', "
11001187
"drive := 'b!abc...', secret := 'ms_graph')",
1101-
"SELECT * FROM graph_excel_delete_rows('report.xlsx', 'Sales', 2, '42', "
1188+
"SELECT * FROM graph_excel_delete_rows('report.xlsx', 'Sales', 0, 'obsolete_row', "
11021189
"site := 'Finance', drive := 'Documents', secret := 'ms_graph')"
11031190
};
11041191
desc.categories = {"microsoft", "graph", "excel"};

src/graph_teams_functions.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ void GraphTeamsFunctions::TeamChannelsScan(
238238
}
239239

240240
// =============================================================================
241-
// graph_team_members Implementation
241+
// graph_teams_members Implementation
242242
// =============================================================================
243243

244244
unique_ptr<FunctionData> GraphTeamsFunctions::TeamMembersBind(
@@ -248,7 +248,7 @@ unique_ptr<FunctionData> GraphTeamsFunctions::TeamMembersBind(
248248
vector<std::string> &names) {
249249

250250
if (input.inputs.empty()) {
251-
throw BinderException("graph_team_members requires a team_id parameter");
251+
throw BinderException("graph_teams_members requires a team_id parameter");
252252
}
253253

254254
auto bind_data = make_uniq<TeamMembersBindData>();
@@ -426,7 +426,7 @@ void GraphTeamsFunctions::Register(ExtensionLoader &loader) {
426426
loader.RegisterFunction(std::move(info));
427427
}
428428
{
429-
TableFunction team_members_func("graph_team_members", {LogicalType::VARCHAR}, TeamMembersScan, TeamMembersBind);
429+
TableFunction team_members_func("graph_teams_members", {LogicalType::VARCHAR}, TeamMembersScan, TeamMembersBind);
430430
team_members_func.named_parameters["secret"] = LogicalType::VARCHAR;
431431
team_members_func.named_parameters["user"] = LogicalType::VARCHAR;
432432
CreateTableFunctionInfo info(team_members_func);
@@ -436,8 +436,8 @@ void GraphTeamsFunctions::Register(ExtensionLoader &loader) {
436436
desc.parameter_names = {"team_id_or_name"};
437437
desc.parameter_types = {LogicalType::VARCHAR};
438438
desc.examples = {
439-
"SELECT * FROM graph_team_members('Engineering', secret := 'ms_graph')",
440-
"SELECT * FROM graph_team_members('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', secret := 'ms_graph')"
439+
"SELECT * FROM graph_teams_members('Engineering', secret := 'ms_graph')",
440+
"SELECT * FROM graph_teams_members('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', secret := 'ms_graph')"
441441
};
442442
desc.categories = {"microsoft", "graph", "teams"};
443443
info.descriptions.push_back(std::move(desc));

0 commit comments

Comments
 (0)