diff --git a/CMakeLists.txt b/CMakeLists.txt index 236ad57..0000b33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,9 @@ set(LOADABLE_EXTENSION_NAME ${TARGET_NAME}_loadable_extension) project(${TARGET_NAME}) -set(CMAKE_CXX_STANDARD "17" CACHE STRING "C++ standard to enforce") +set(CMAKE_CXX_STANDARD + "17" + CACHE STRING "C++ standard to enforce") set(CMAKE_CXX_STANDARD_REQUIRED ON) include_directories(src/include) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 9af93a2..6d19b00 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -70,6 +70,11 @@ Also included: ### Housekeeping +- Functions are discoverable from `duckdb_functions()`: each of `table_diff`, + `table_diff_summary` and `schema_diff` carries a description, a runnable + example and categories, and the two relation arguments are named + `left` / `right` instead of `col0` / `col1` + ([#11](https://github.com/avaitla/duck_diff/issues/11)). - Upgraded the bundled DuckDB to v2.0.0 (extension-ci-tools `v2.0-cyanoptera`). - `duck_diff` is now installable from the DuckDB community extension repository: `INSTALL duck_diff FROM community;` diff --git a/src/duck_diff_extension.cpp b/src/duck_diff_extension.cpp index a4822cf..9ce2139 100644 --- a/src/duck_diff_extension.cpp +++ b/src/duck_diff_extension.cpp @@ -6,6 +6,7 @@ #include "duckdb/common/string_util.hpp" #include "duckdb/common/case_insensitive_map.hpp" #include "duckdb/function/table_function.hpp" +#include "duckdb/parser/parsed_data/create_table_function_info.hpp" #include "duckdb/parser/parser.hpp" #include "duckdb/parser/statement/select_statement.hpp" #include "duckdb/parser/tableref/subqueryref.hpp" @@ -705,6 +706,30 @@ unique_ptr SchemaDiffBindReplace(ClientContext &context, TableFunction // Registration //===--------------------------------------------------------------------===// +// A table function taking the two relation query strings as positional-only +// parameters named left / right, so duckdb_functions() reports real names +// rather than col0 / col1. +TableFunction MakeRelationPairFunction(const char *name, table_function_bind_replace_t bind_replace) { + TableFunction fun(name, vector(), nullptr, nullptr); + fun.bind_replace = bind_replace; + fun.GetSignature().AddPositionalOnly("left", LogicalType::VARCHAR).AddPositionalOnly("right", LogicalType::VARCHAR); + return fun; +} + +// Register with a FunctionDescription so the function is discoverable from +// duckdb_functions() (description / examples / categories). +void RegisterDescribed(ExtensionLoader &loader, TableFunction fun, const string &description, const string &example) { + CreateTableFunctionInfo info(std::move(fun)); + FunctionDescription desc; + desc.description = description; + desc.examples = {example}; + desc.categories = {"diff", "data_quality"}; + info.descriptions.push_back(std::move(desc)); + // What the bare RegisterFunction(TableFunction) overload sets; CreateInfo defaults to ERROR_ON_CONFLICT. + info.on_conflict = OnCreateConflict::ALTER_ON_CONFLICT; + loader.RegisterFunction(std::move(info)); +} + void AddDiffParameters(TableFunction &fun) { // All optional: a NULL default reads as "not passed" (see FindParam). auto &sig = fun.GetSignature(); @@ -721,20 +746,41 @@ void AddDiffParameters(TableFunction &fun) { } void LoadInternal(ExtensionLoader &loader) { - TableFunction table_diff("table_diff", {LogicalType::VARCHAR, LogicalType::VARCHAR}, nullptr, nullptr); - table_diff.bind_replace = TableDiffBindReplace; + auto table_diff = MakeRelationPairFunction("table_diff", TableDiffBindReplace); AddDiffParameters(table_diff); - loader.RegisterFunction(table_diff); - - TableFunction table_diff_summary("table_diff_summary", {LogicalType::VARCHAR, LogicalType::VARCHAR}, nullptr, - nullptr); - table_diff_summary.bind_replace = TableDiffSummaryBindReplace; + RegisterDescribed(loader, std::move(table_diff), + "Diffs two relations (query strings, e.g. 'FROM t') on a primary key: one row per key with " + "diff_status (identical/different/left_only/right_only), a JSON diff_data of the changed " + "columns, and per-column _left / _right / _diff_status.", + "SELECT id, diff_status, diff_data FROM table_diff(" + "'FROM (VALUES (1, 10), (2, 20), (3, 30)) t(id, v)', " + "'FROM (VALUES (1, 10), (2, 25), (4, 40)) t(id, v)', pk := 'id') ORDER BY id;\n" + "-- 1 | identical | NULL\n" + "-- 2 | different | {\"v\":{\"left\":20,\"right\":25}}\n" + "-- 3 | left_only | NULL\n" + "-- 4 | right_only | NULL"); + + auto table_diff_summary = MakeRelationPairFunction("table_diff_summary", TableDiffSummaryBindReplace); AddDiffParameters(table_diff_summary); - loader.RegisterFunction(table_diff_summary); - - TableFunction schema_diff("schema_diff", {LogicalType::VARCHAR, LogicalType::VARCHAR}, nullptr, nullptr); - schema_diff.bind_replace = SchemaDiffBindReplace; - loader.RegisterFunction(schema_diff); + RegisterDescribed(loader, std::move(table_diff_summary), + "Summarizes a table_diff of two relations in one row: per-status counts (n_identical, " + "n_different, n_left_only, n_right_only, n_total) and their percentages. Takes the same " + "parameters as table_diff.", + "SELECT n_identical, n_different, n_left_only, n_right_only FROM table_diff_summary(" + "'FROM (VALUES (1, 10), (2, 20), (3, 30)) t(id, v)', " + "'FROM (VALUES (1, 10), (2, 25), (4, 40)) t(id, v)', pk := 'id');\n" + "-- 1 | 1 | 1 | 1"); + + auto schema_diff = MakeRelationPairFunction("schema_diff", SchemaDiffBindReplace); + RegisterDescribed(loader, std::move(schema_diff), + "Compares the column names and types of two relations (query strings) without reading any " + "rows: one row per column with left_type, right_type and status " + "(identical/type_differs/left_only/right_only).", + "SELECT * FROM schema_diff('SELECT 1::INTEGER AS id, ''a'' AS name', " + "'SELECT 1::BIGINT AS id, 2.5 AS score');\n" + "-- id | INTEGER | BIGINT | type_differs\n" + "-- name | VARCHAR | NULL | left_only\n" + "-- score | NULL | DECIMAL(2,1) | right_only"); } } // namespace diff --git a/test/sql/function_metadata.test b/test/sql/function_metadata.test new file mode 100644 index 0000000..4853a7a --- /dev/null +++ b/test/sql/function_metadata.test @@ -0,0 +1,50 @@ +# name: test/sql/function_metadata.test +# description: duck_diff functions are discoverable from duckdb_functions() -- real parameter names, description, examples, categories +# group: [sql] + +require duck_diff + +require json + +# positional parameters are named left / right, not col0 / col1 +query II +SELECT function_name, parameters[1:2] +FROM duckdb_functions() +WHERE function_name IN ('table_diff', 'table_diff_summary', 'schema_diff') +ORDER BY function_name; +---- +schema_diff [left, right] +table_diff [left, right] +table_diff_summary [left, right] + +# every function has a description, one example, and categories +query IIII +SELECT function_name, description <> '', len(examples), categories +FROM duckdb_functions() +WHERE function_name IN ('table_diff', 'table_diff_summary', 'schema_diff') +ORDER BY function_name; +---- +schema_diff true 1 [diff, data_quality] +table_diff true 1 [diff, data_quality] +table_diff_summary true 1 [diff, data_quality] + +# the examples are runnable as written (each mirrors the query below) +query III +SELECT id, diff_status, diff_data FROM table_diff('FROM (VALUES (1, 10), (2, 20), (3, 30)) t(id, v)', 'FROM (VALUES (1, 10), (2, 25), (4, 40)) t(id, v)', pk := 'id') ORDER BY id; +---- +1 identical NULL +2 different {"v":{"left":20,"right":25}} +3 left_only NULL +4 right_only NULL + +query IIII +SELECT n_identical, n_different, n_left_only, n_right_only FROM table_diff_summary('FROM (VALUES (1, 10), (2, 20), (3, 30)) t(id, v)', 'FROM (VALUES (1, 10), (2, 25), (4, 40)) t(id, v)', pk := 'id'); +---- +1 1 1 1 + +query IIII +SELECT * FROM schema_diff('SELECT 1::INTEGER AS id, ''a'' AS name', 'SELECT 1::BIGINT AS id, 2.5 AS score'); +---- +id INTEGER BIGINT type_differs +name VARCHAR NULL left_only +score NULL DECIMAL(2,1) right_only