Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: duckdb_api and custom_user_agent options #9603

Merged
merged 6 commits into from Nov 9, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/function/pragma/pragma_queries.cpp
Expand Up @@ -194,6 +194,10 @@ string PragmaMetadataInfo(ClientContext &context, const FunctionParameters &para
return "SELECT * FROM pragma_metadata_info();";
}

string PragmaUserAgent(ClientContext &context, const FunctionParameters &parameters) {
return "SELECT * FROM pragma_user_agent()";
}

void PragmaQueries::RegisterFunction(BuiltinFunctions &set) {
set.AddFunction(PragmaFunction::PragmaCall("table_info", PragmaTableInfo, {LogicalType::VARCHAR}));
set.AddFunction(PragmaFunction::PragmaCall("storage_info", PragmaStorageInfo, {LogicalType::VARCHAR}));
Expand All @@ -210,6 +214,7 @@ void PragmaQueries::RegisterFunction(BuiltinFunctions &set) {
set.AddFunction(PragmaFunction::PragmaStatement("functions", PragmaFunctionsQuery));
set.AddFunction(PragmaFunction::PragmaCall("import_database", PragmaImportDatabase, {LogicalType::VARCHAR}));
set.AddFunction(PragmaFunction::PragmaStatement("all_profiling_output", PragmaAllProfiling));
set.AddFunction(PragmaFunction::PragmaStatement("user_agent", PragmaUserAgent));
}

} // namespace duckdb
1 change: 1 addition & 0 deletions src/function/table/system/CMakeLists.txt
Expand Up @@ -21,6 +21,7 @@ add_library_unity(
pragma_metadata_info.cpp
pragma_storage_info.cpp
pragma_table_info.cpp
pragma_user_agent.cpp
test_all_types.cpp
test_vector_types.cpp)
set(ALL_OBJECT_FILES
Expand Down
50 changes: 50 additions & 0 deletions src/function/table/system/pragma_user_agent.cpp
@@ -0,0 +1,50 @@
#include "duckdb/function/table/system_functions.hpp"
#include "duckdb/main/config.hpp"

namespace duckdb {

struct PragmaUserAgentData : public GlobalTableFunctionState {
PragmaUserAgentData() : finished(false) {
}

std::string user_agent;
bool finished;
};

static unique_ptr<FunctionData> PragmaUserAgentBind(ClientContext &context, TableFunctionBindInput &input,
vector<LogicalType> &return_types, vector<string> &names) {

names.emplace_back("user_agent");
return_types.emplace_back(LogicalType::VARCHAR);

return nullptr;
}

unique_ptr<GlobalTableFunctionState> PragmaUserAgentInit(ClientContext &context, TableFunctionInitInput &input) {
auto result = make_uniq<PragmaUserAgentData>();
auto &config = DBConfig::GetConfig(context);
result->user_agent = config.UserAgent();

return std::move(result);
}

void PragmaUserAgentFunction(ClientContext &context, TableFunctionInput &data_p, DataChunk &output) {
auto &data = data_p.global_state->Cast<PragmaUserAgentData>();

if (data.finished) {
// signal end of output
return;
}

output.SetCardinality(1);
output.SetValue(0, 0, data.user_agent);

data.finished = true;
}

void PragmaUserAgent::RegisterFunction(BuiltinFunctions &set) {
set.AddFunction(
TableFunction("pragma_user_agent", {}, PragmaUserAgentFunction, PragmaUserAgentBind, PragmaUserAgentInit));
}

} // namespace duckdb
1 change: 1 addition & 0 deletions src/function/table/system_functions.cpp
Expand Up @@ -18,6 +18,7 @@ void BuiltinFunctions::RegisterSQLiteFunctions() {
PragmaDatabaseSize::RegisterFunction(*this);
PragmaLastProfilingOutput::RegisterFunction(*this);
PragmaDetailedProfilingOutput::RegisterFunction(*this);
PragmaUserAgent::RegisterFunction(*this);

DuckDBColumnsFun::RegisterFunction(*this);
DuckDBConstraintsFun::RegisterFunction(*this);
Expand Down
4 changes: 4 additions & 0 deletions src/include/duckdb/function/table/system_functions.hpp
Expand Up @@ -133,4 +133,8 @@ struct TestVectorTypesFun {
static void RegisterFunction(BuiltinFunctions &set);
};

struct PragmaUserAgent {
static void RegisterFunction(BuiltinFunctions &set);
};

} // namespace duckdb
5 changes: 5 additions & 0 deletions src/include/duckdb/main/config.hpp
Expand Up @@ -173,6 +173,10 @@ struct DBConfigOptions {
static bool debug_print_bindings;
//! The peak allocation threshold at which to flush the allocator after completing a task (1 << 27, ~128MB)
idx_t allocator_flush_threshold = 134217728;
//! DuckDB API surface
string duckdb_api;
//! Metadata from DuckDB callers
string custom_user_agent;

bool operator==(const DBConfigOptions &other) const;
};
Expand Down Expand Up @@ -259,6 +263,7 @@ struct DBConfig {

OrderType ResolveOrder(OrderType order_type) const;
OrderByNullType ResolveNullOrder(OrderType order_type, OrderByNullType null_type) const;
const std::string UserAgent() const;

private:
unique_ptr<CompressionFunctionSet> compression_functions;
Expand Down
18 changes: 18 additions & 0 deletions src/include/duckdb/main/settings.hpp
Expand Up @@ -552,4 +552,22 @@ struct FlushAllocatorSetting {
static Value GetSetting(ClientContext &context);
};

struct DuckDBApiSetting {
static constexpr const char *Name = "duckdb_api";
static constexpr const char *Description = "DuckDB API surface";
static constexpr const LogicalTypeId InputType = LogicalTypeId::VARCHAR;
static void SetGlobal(DatabaseInstance *db, DBConfig &config, const Value &parameter);
static void ResetGlobal(DatabaseInstance *db, DBConfig &config);
static Value GetSetting(ClientContext &context);
};

struct CustomUserAgentSetting {
static constexpr const char *Name = "custom_user_agent";
static constexpr const char *Description = "Metadata from DuckDB callers";
static constexpr const LogicalTypeId InputType = LogicalTypeId::VARCHAR;
static void SetGlobal(DatabaseInstance *db, DBConfig &config, const Value &parameter);
static void ResetGlobal(DatabaseInstance *db, DBConfig &config);
static Value GetSetting(ClientContext &context);
};

} // namespace duckdb
1 change: 1 addition & 0 deletions src/main/capi/config-c.cpp
Expand Up @@ -13,6 +13,7 @@ duckdb_state duckdb_create_config(duckdb_config *out_config) {
DBConfig *config;
try {
config = new DBConfig();
config->SetOptionByName("duckdb_api", "capi");
} catch (...) { // LCOV_EXCL_START
return DuckDBError;
} // LCOV_EXCL_STOP
Expand Down
10 changes: 9 additions & 1 deletion src/main/capi/duckdb-c.cpp
Expand Up @@ -8,7 +8,15 @@ using duckdb::DuckDB;
duckdb_state duckdb_open_ext(const char *path, duckdb_database *out, duckdb_config config, char **error) {
auto wrapper = new DatabaseData();
try {
auto db_config = (DBConfig *)config;
DBConfig default_config;
default_config.SetOptionByName("duckdb_api", "capi");

DBConfig *db_config = &default_config;
DBConfig *user_config = (DBConfig *)config;
if (user_config) {
db_config = user_config;
}

wrapper->database = duckdb::make_uniq<DuckDB>(path, db_config);
} catch (std::exception &ex) {
if (error) {
Expand Down
11 changes: 11 additions & 0 deletions src/main/config.cpp
Expand Up @@ -115,6 +115,8 @@ static ConfigurationOption internal_options[] = {DUCKDB_GLOBAL(AccessModeSetting
DUCKDB_GLOBAL_ALIAS("wal_autocheckpoint", CheckpointThresholdSetting),
DUCKDB_GLOBAL_ALIAS("worker_threads", ThreadsSetting),
DUCKDB_GLOBAL(FlushAllocatorSetting),
DUCKDB_GLOBAL(DuckDBApiSetting),
DUCKDB_GLOBAL(CustomUserAgentSetting),
FINAL_SETTING};

vector<ConfigurationOption> DBConfig::GetOptions() {
Expand Down Expand Up @@ -418,4 +420,13 @@ OrderByNullType DBConfig::ResolveNullOrder(OrderType order_type, OrderByNullType
}
}

const std::string DBConfig::UserAgent() const {
auto user_agent = options.duckdb_api;

if (!options.custom_user_agent.empty()) {
user_agent += " " + options.custom_user_agent;
}
return user_agent;
}

} // namespace duckdb
1 change: 1 addition & 0 deletions src/main/database.cpp
Expand Up @@ -31,6 +31,7 @@ DBConfig::DBConfig() {
compression_functions = make_uniq<CompressionFunctionSet>();
cast_functions = make_uniq<CastFunctionSet>();
error_manager = make_uniq<ErrorManager>();
options.duckdb_api = StringUtil::Format("duckdb/%s(%s)", DuckDB::LibraryVersion(), DuckDB::Platform());
}

DBConfig::DBConfig(std::unordered_map<string, string> &config_dict, bool read_only) : DBConfig::DBConfig() {
Expand Down
49 changes: 49 additions & 0 deletions src/main/settings/settings.cpp
Expand Up @@ -1203,4 +1203,53 @@ Value FlushAllocatorSetting::GetSetting(ClientContext &context) {
return Value(StringUtil::BytesToHumanReadableString(config.options.allocator_flush_threshold));
}

//===--------------------------------------------------------------------===//
// DuckDBApi Setting
//===--------------------------------------------------------------------===//

void DuckDBApiSetting::SetGlobal(DatabaseInstance *db, DBConfig &config, const Value &input) {
auto new_value = input.GetValue<string>();
if (db) {
throw InvalidInputException("Cannot change duckdb_api setting while database is running");
}
config.options.duckdb_api += " " + new_value;
}

void DuckDBApiSetting::ResetGlobal(DatabaseInstance *db, DBConfig &config) {
if (db) {
throw InvalidInputException("Cannot change duckdb_api setting while database is running");
}
config.options.duckdb_api = DBConfig().options.duckdb_api;
}

Value DuckDBApiSetting::GetSetting(ClientContext &context) {
auto &config = DBConfig::GetConfig(context);
return Value(config.options.duckdb_api);
}

//===--------------------------------------------------------------------===//
// CustomUserAgent Setting
//===--------------------------------------------------------------------===//

void CustomUserAgentSetting::SetGlobal(DatabaseInstance *db, DBConfig &config, const Value &input) {
auto new_value = input.GetValue<string>();
if (db) {
throw InvalidInputException("Cannot change custom_user_agent setting while database is running");
}
config.options.custom_user_agent =
config.options.custom_user_agent.empty() ? new_value : config.options.custom_user_agent + " " + new_value;
}

void CustomUserAgentSetting::ResetGlobal(DatabaseInstance *db, DBConfig &config) {
if (db) {
throw InvalidInputException("Cannot change custom_user_agent setting while database is running");
}
config.options.custom_user_agent = DBConfig().options.custom_user_agent;
}

Value CustomUserAgentSetting::GetSetting(ClientContext &context) {
auto &config = DBConfig::GetConfig(context);
return Value(config.options.custom_user_agent);
}

} // namespace duckdb
62 changes: 62 additions & 0 deletions test/api/capi/test_capi.cpp
@@ -1,5 +1,7 @@
#include "capi_tester.hpp"

#include <regex>

using namespace duckdb;
using namespace std;

Expand Down Expand Up @@ -573,3 +575,63 @@ TEST_CASE("Decimal -> Double casting issue", "[capi]") {
auto string_from_decimal = result->Fetch<string>(0, 0);
REQUIRE(string_from_decimal == "-0.5");
}

TEST_CASE("Test custom_user_agent config", "[capi]") {

{
duckdb_database db;
duckdb_connection con;
duckdb_result result;

// Default custom_user_agent value
REQUIRE(duckdb_open_ext(NULL, &db, nullptr, NULL) != DuckDBError);
REQUIRE(duckdb_connect(db, &con) != DuckDBError);

duckdb_query(con, "PRAGMA user_agent", &result);

REQUIRE(duckdb_row_count(&result) == 1);
char *user_agent_value = duckdb_value_varchar(&result, 0, 0);
REQUIRE_THAT(user_agent_value, Catch::Matchers::Matches("duckdb/.*(.*) capi"));

duckdb_free(user_agent_value);
duckdb_destroy_result(&result);
duckdb_disconnect(&con);
duckdb_close(&db);
}

{
// Custom custom_user_agent value

duckdb_database db;
duckdb_connection con;
duckdb_result result_custom_user_agent;
duckdb_result result_full_user_agent;

duckdb_config config;
REQUIRE(duckdb_create_config(&config) != DuckDBError);
REQUIRE(duckdb_set_config(config, "custom_user_agent", "CUSTOM_STRING") != DuckDBError);

REQUIRE(duckdb_open_ext(NULL, &db, config, NULL) != DuckDBError);
REQUIRE(duckdb_connect(db, &con) != DuckDBError);

duckdb_query(con, "SELECT current_setting('custom_user_agent')", &result_custom_user_agent);
duckdb_query(con, "PRAGMA user_agent", &result_full_user_agent);

REQUIRE(duckdb_row_count(&result_custom_user_agent) == 1);
REQUIRE(duckdb_row_count(&result_full_user_agent) == 1);

char *custom_user_agent_value = duckdb_value_varchar(&result_custom_user_agent, 0, 0);
REQUIRE(string(custom_user_agent_value) == "CUSTOM_STRING");

char *full_user_agent_value = duckdb_value_varchar(&result_full_user_agent, 0, 0);
REQUIRE_THAT(full_user_agent_value, Catch::Matchers::Matches("duckdb/.*(.*) capi CUSTOM_STRING"));

duckdb_destroy_config(&config);
duckdb_free(custom_user_agent_value);
duckdb_free(full_user_agent_value);
duckdb_destroy_result(&result_custom_user_agent);
duckdb_destroy_result(&result_full_user_agent);
duckdb_disconnect(&con);
duckdb_close(&db);
}
}
4 changes: 3 additions & 1 deletion test/api/test_reset.cpp
Expand Up @@ -125,7 +125,9 @@ bool OptionIsExcludedFromTest(const string &name) {
"username",
"user",
"profiling_output", // just an alias
"profiler_history_size"};
"profiler_history_size",
"duckdb_api",
"custom_user_agent"};
return excluded_options.count(name) == 1;
}

Expand Down
28 changes: 28 additions & 0 deletions test/sql/settings/user_agent.test
@@ -0,0 +1,28 @@
# name: test/sql/settings/user_agent.test
# description: Test user agent setting
# group: [settings]

statement error
SET custom_user_agent='something else'
----
Cannot change custom_user_agent setting while database is running

statement error
RESET custom_user_agent
----
Cannot change custom_user_agent setting while database is running

query T
SELECT current_setting('custom_user_agent')
----
(empty)

statement error
SET duckdb_api='something else'
----
Cannot change duckdb_api setting while database is running

query T
SELECT regexp_matches(user_agent, '^duckdb/.*(.*)') FROM pragma_user_agent()
----
true
1 change: 1 addition & 0 deletions tools/jdbc/src/jni/duckdb_java.cpp
Expand Up @@ -310,6 +310,7 @@ static const char *const JDBC_STREAM_RESULTS = "jdbc_stream_results";
jobject _duckdb_jdbc_startup(JNIEnv *env, jclass, jbyteArray database_j, jboolean read_only, jobject props) {
auto database = byte_array_to_string(env, database_j);
DBConfig config;
config.SetOptionByName("duckdb_api", "java");
config.AddExtensionOption(
JDBC_STREAM_RESULTS,
"Whether to stream results. Only one ResultSet on a connection can be open at once when true",
Expand Down
2 changes: 2 additions & 0 deletions tools/jdbc/src/main/java/org/duckdb/DuckDBDriver.java
Expand Up @@ -11,6 +11,7 @@
public class DuckDBDriver implements java.sql.Driver {

public static final String DUCKDB_READONLY_PROPERTY = "duckdb.read_only";
public static final String DUCKDB_USER_AGENT_PROPERTY = "custom_user_agent";
public static final String JDBC_STREAM_RESULTS = "jdbc_stream_results";

static {
Expand All @@ -36,6 +37,7 @@ public Connection connect(String url, Properties info) throws SQLException {
String prop_clean = prop_val.trim().toLowerCase();
read_only = prop_clean.equals("1") || prop_clean.equals("true") || prop_clean.equals("yes");
}
info.put("duckdb_api", "jdbc");
return DuckDBConnection.newConnection(url, read_only, info);
}

Expand Down