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 3 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
9 changes: 9 additions & 0 deletions src/include/duckdb/main/settings.hpp
Expand Up @@ -552,4 +552,13 @@ struct FlushAllocatorSetting {
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
6 changes: 5 additions & 1 deletion src/main/capi/config-c.cpp
Expand Up @@ -45,7 +45,11 @@ duckdb_state duckdb_set_config(duckdb_config config, const char *name, const cha

try {
auto db_config = (DBConfig *)config;
db_config->SetOptionByName(name, Value(option));
if (strcmp(name, "duckdb_api") == 0) {
db_config->options.duckdb_api = std::string(option);
} else {
db_config->SetOptionByName(name, Value(option));
elefeint marked this conversation as resolved.
Show resolved Hide resolved
}
} catch (...) {
return DuckDBError;
}
Expand Down
11 changes: 10 additions & 1 deletion src/main/capi/duckdb-c.cpp
Expand Up @@ -8,7 +8,16 @@ 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;
DBConfig *db_config = &default_config;
DBConfig *user_config = (DBConfig *)config;
if (user_config) {
db_config = user_config;
}

if (db_config->options.duckdb_api.empty()) {
db_config->options.duckdb_api = "capi";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of doing the check here perhaps it's cleaner/easier to set duckdb_api to "capi" in duckdb_create_config?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some logic for setting capi is still needed here because duckdb_open_ext() is often called with nullptr passed in for config.

}
wrapper->database = duckdb::make_uniq<DuckDB>(path, db_config);
} catch (std::exception &ex) {
if (error) {
Expand Down
13 changes: 13 additions & 0 deletions src/main/config.cpp
@@ -1,4 +1,5 @@
#include "duckdb/main/config.hpp"
#include "duckdb/main/database.hpp"

#include "duckdb/common/operator/cast_operators.hpp"
#include "duckdb/common/string_util.hpp"
Expand Down Expand Up @@ -115,6 +116,7 @@ static ConfigurationOption internal_options[] = {DUCKDB_GLOBAL(AccessModeSetting
DUCKDB_GLOBAL_ALIAS("wal_autocheckpoint", CheckpointThresholdSetting),
DUCKDB_GLOBAL_ALIAS("worker_threads", ThreadsSetting),
DUCKDB_GLOBAL(FlushAllocatorSetting),
DUCKDB_GLOBAL(CustomUserAgentSetting),
FINAL_SETTING};

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

const std::string DBConfig::UserAgent() const {
auto user_agent = StringUtil::Format("duckdb/%s(%s)", DuckDB::LibraryVersion(), DuckDB::Platform());
if (!options.duckdb_api.empty()) {
user_agent += " " + options.duckdb_api;
}
if (!options.custom_user_agent.empty()) {
user_agent += " " + options.custom_user_agent;
}
return user_agent;
}

} // namespace duckdb
24 changes: 24 additions & 0 deletions src/main/settings/settings.cpp
Expand Up @@ -1203,4 +1203,28 @@ Value FlushAllocatorSetting::GetSetting(ClientContext &context) {
return Value(StringUtil::BytesToHumanReadableString(config.options.allocator_flush_threshold));
}

//===--------------------------------------------------------------------===//
// UserAgent 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 = 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
63 changes: 63 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,64 @@ 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 value FROM duckdb_settings() WHERE name = '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 @@ -102,6 +102,7 @@ OptionValueSet &GetValueForOption(const string &name) {
{"force_bitpacking_mode", {"constant"}},
{"allocator_flush_threshold", {"4.2GB"}},
{"arrow_large_buffer_size", {true}}};

// Every option that's not excluded has to be part of this map
if (!value_map.count(name)) {
REQUIRE(name == "MISSING_FROM_MAP");
Expand All @@ -125,7 +126,8 @@ bool OptionIsExcludedFromTest(const string &name) {
"username",
"user",
"profiling_output", // just an alias
"profiler_history_size"};
"profiler_history_size",
"custom_user_agent"};
return excluded_options.count(name) == 1;
}

Expand Down
23 changes: 23 additions & 0 deletions test/sql/settings/user_agent.test
@@ -0,0 +1,23 @@
# 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 value FROM duckdb_settings() WHERE name = 'custom_user_agent'
----
(empty)

query T
SELECT regexp_matches(user_agent, '^duckdb/.*(.*)') FROM pragma_user_agent()
----
true
11 changes: 11 additions & 0 deletions tools/jdbc/src/jni/duckdb_java.cpp
Expand Up @@ -72,6 +72,7 @@ static jmethodID J_DuckStruct_init;
static jclass J_ByteBuffer;

static jmethodID J_Map_entrySet;
static jmethodID J_Map_remove;
static jmethodID J_Set_iterator;
static jmethodID J_Iterator_hasNext;
static jmethodID J_Iterator_next;
Expand Down Expand Up @@ -147,6 +148,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {

tmpLocalRef = env->FindClass("java/util/Map");
J_Map_entrySet = env->GetMethodID(tmpLocalRef, "entrySet", "()Ljava/util/Set;");
J_Map_remove = env->GetMethodID(tmpLocalRef, "remove", "(Ljava/lang/Object;)Ljava/lang/Object;");
env->DeleteLocalRef(tmpLocalRef);

tmpLocalRef = env->FindClass("java/util/Set");
Expand Down Expand Up @@ -310,13 +312,22 @@ 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.options.duckdb_api = "jni";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not call this Java? You also seem to be overriding it in the Java layer, which is a bit confusing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I'll rename to "java". This override is here just in case someone uses Java without JDBC -- making the distinction between "java" and "jdbc" would make this obscure case easier to troubleshoot.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if that would be possible? The Java API is the the JDBC API

config.AddExtensionOption(
JDBC_STREAM_RESULTS,
"Whether to stream results. Only one ResultSet on a connection can be open at once when true",
LogicalType::BOOLEAN);
if (read_only) {
config.options.access_mode = AccessMode::READ_ONLY;
}
const char *duckdb_api_key = "duckdb_api";
jstring jduckdb_api_key = env->NewStringUTF(duckdb_api_key);
jobject duckdb_api_value = env->CallObjectMethod(props, J_Map_remove, jduckdb_api_key);
if (duckdb_api_value) {
elefeint marked this conversation as resolved.
Show resolved Hide resolved
D_ASSERT(env->IsInstanceOf(duckdb_api_value, J_String));
const string &duckdb_api_value_str = jstring_to_string(env, (jstring)duckdb_api_value);
config.options.duckdb_api = duckdb_api_value_str;
}
jobject entry_set = env->CallObjectMethod(props, J_Map_entrySet);
jobject iterator = env->CallObjectMethod(entry_set, J_Set_iterator);

Expand Down
3 changes: 3 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 @@ -32,6 +33,8 @@ public Connection connect(String url, Properties info) throws SQLException {
info = (Properties) info.clone();
}
String prop_val = (String) info.remove(DUCKDB_READONLY_PROPERTY);
info.put("duckdb_api", "jdbc");

if (prop_val != null) {
String prop_clean = prop_val.trim().toLowerCase();
read_only = prop_clean.equals("1") || prop_clean.equals("true") || prop_clean.equals("yes");
Expand Down