Skip to content

Commit c46c7e8

Browse files
committed
add and use function builder to document, update function reference generator script
1 parent 1f3068c commit c46c7e8

20 files changed

+535
-205
lines changed

docs/functions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1778,7 +1778,7 @@ MULTIPOINT Z EMPTY
17781778
#### Signatures
17791779

17801780
```sql
1781-
VARCHAR ST_QuadKey (longitude DOUBLE, latitude DOUBLE, level INTEGER)
1781+
VARCHAR ST_QuadKey (lon DOUBLE, lat DOUBLE, level INTEGER)
17821782
VARCHAR ST_QuadKey (point GEOMETRY, level INTEGER)
17831783
```
17841784

@@ -2350,7 +2350,7 @@ SELECT * FROM ST_Drivers();
23502350
#### Signature
23512351

23522352
```sql
2353-
ST_Read (col0 VARCHAR, keep_wkb BOOLEAN, max_batch_size INTEGER, sequential_layer_scan BOOLEAN, layer VARCHAR, spatial_filter WKB_BLOB, spatial_filter_box BOX_2D, sibling_files VARCHAR[], allowed_drivers VARCHAR[], open_options VARCHAR[])
2353+
ST_Read (col0 VARCHAR, keep_wkb BOOLEAN, max_batch_size INTEGER, sequential_layer_scan BOOLEAN, layer VARCHAR, sibling_files VARCHAR[], spatial_filter WKB_BLOB, spatial_filter_box BOX_2D, allowed_drivers VARCHAR[], open_options VARCHAR[])
23542354
```
23552355

23562356
#### Description

extension_config.cmake

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,5 @@ duckdb_extension_load(spatial
1313
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}
1414
INCLUDE_DIR ${CMAKE_CURRENT_LIST_DIR}/spatial/include
1515
${DO_TESTS}
16-
DONT_LINK
1716
LINKED_LIBS "../../deps/local/lib/*.a"
1817
)

generate_function_reference.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import json
33

4+
# We just take the first non-empty description and example for now
45
get_spatial_functions_sql = """
56
SELECT
67
json({
@@ -16,15 +17,17 @@
1617
function_name,
1718
list({
1819
return: return_type,
19-
params: list_zip(parameters, parameter_types)::STRUCT(name VARCHAR, type VARCHAR)[]
20+
params: list_zip(parameters, parameter_types)::STRUCT(name VARCHAR, type VARCHAR)[],
21+
description: description,
22+
examples: examples
2023
}) as signatures,
24+
list_filter(signatures, x -> x.description IS NOT NULL)[1].description as description,
25+
list_filter(signatures, x -> len(x.examples) != 0)[1].examples[1] as example,
2126
any_value(tags) AS func_tags,
22-
any_value(description) AS description,
23-
any_value(example) AS example
2427
FROM duckdb_functions() as funcs
2528
WHERE function_type = '$FUNCTION_TYPE$'
2629
GROUP BY function_name, function_type
27-
HAVING func_tags['ext'] = ['spatial']
30+
HAVING func_tags['ext'] = 'spatial'
2831
ORDER BY function_name
2932
);
3033
"""
@@ -40,7 +43,7 @@ def write_table_of_contents(f, functions):
4043
f.write('| --- | --- |\n')
4144
for function in functions:
4245
# Summary is the first line of the description
43-
summary = function['description'].split('\n')[0]
46+
summary = function['description'].split('\n')[0] if function['description'] else ""
4447
f.write(f"| [`{function['name']}`](#{to_kebab_case(function['name'])}) | {summary} |\n")
4548

4649

@@ -87,6 +90,8 @@ def main():
8790
f.write(f"{signature['return']} {function['name']} ({param_list})\n")
8891
f.write("```\n\n")
8992

93+
94+
9095
if function['description']:
9196
f.write("#### Description\n\n")
9297
f.write(function['description'])
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#pragma once
2+
3+
#include "duckdb.hpp"
4+
5+
#include "spatial/common.hpp"
6+
#include "duckdb/function/function_set.hpp"
7+
#include "duckdb/function/scalar_function.hpp"
8+
#include "duckdb/parser/parsed_data/create_function_info.hpp"
9+
10+
namespace spatial {
11+
12+
namespace core {
13+
//------------------------------------------------------------------------------
14+
// Scalar Function Variant Builder
15+
//------------------------------------------------------------------------------
16+
17+
class ScalarFunctionVariantBuilder {
18+
friend class ScalarFunctionBuilder;
19+
20+
public:
21+
void AddParameter(const char *name, const LogicalType &type);
22+
void SetReturnType(LogicalType type);
23+
void SetFunction(scalar_function_t fn);
24+
void SetInit(init_local_state_t init);
25+
void SetBind(bind_scalar_function_t bind);
26+
void SetDescription(const string &desc);
27+
void SetExample(const string &ex);
28+
29+
private:
30+
explicit ScalarFunctionVariantBuilder() : function({}, LogicalTypeId::INVALID, nullptr) {
31+
}
32+
33+
ScalarFunction function;
34+
FunctionDescription description = {};
35+
};
36+
37+
inline void ScalarFunctionVariantBuilder::AddParameter(const char *name, const LogicalType &type) {
38+
function.arguments.emplace_back(type);
39+
description.parameter_names.emplace_back(name);
40+
description.parameter_types.emplace_back(type);
41+
}
42+
43+
inline void ScalarFunctionVariantBuilder::SetReturnType(LogicalType type) {
44+
function.return_type = std::move(type);
45+
}
46+
47+
inline void ScalarFunctionVariantBuilder::SetFunction(scalar_function_t fn) {
48+
function.function = fn;
49+
}
50+
51+
inline void ScalarFunctionVariantBuilder::SetInit(init_local_state_t init) {
52+
function.init_local_state = init;
53+
}
54+
55+
inline void ScalarFunctionVariantBuilder::SetBind(bind_scalar_function_t bind) {
56+
function.bind = bind;
57+
}
58+
59+
inline void ScalarFunctionVariantBuilder::SetDescription(const string &desc) {
60+
description.description = desc;
61+
}
62+
63+
inline void ScalarFunctionVariantBuilder::SetExample(const string &ex) {
64+
description.examples.emplace_back(ex);
65+
}
66+
67+
//------------------------------------------------------------------------------
68+
// Scalar Function Builder
69+
//------------------------------------------------------------------------------
70+
71+
class ScalarFunctionBuilder {
72+
friend class FunctionBuilder;
73+
74+
public:
75+
template <class CALLBACK>
76+
void AddVariant(CALLBACK &&callback);
77+
void SetTag(const string &key, const string &value);
78+
void SetDescription(const string &desc);
79+
80+
private:
81+
explicit ScalarFunctionBuilder(const char *name) : set(name) {
82+
}
83+
84+
ScalarFunctionSet set;
85+
vector<FunctionDescription> descriptions = {};
86+
unordered_map<string, string> tags = {};
87+
88+
// If not set by a variant
89+
string default_description;
90+
};
91+
92+
inline void ScalarFunctionBuilder::SetDescription(const string &desc) {
93+
default_description = desc;
94+
}
95+
96+
inline void ScalarFunctionBuilder::SetTag(const string &key, const string &value) {
97+
tags[key] = value;
98+
}
99+
100+
template <class CALLBACK>
101+
void ScalarFunctionBuilder::AddVariant(CALLBACK &&callback) {
102+
ScalarFunctionVariantBuilder builder;
103+
104+
callback(builder);
105+
106+
// A return type is required
107+
if (builder.function.return_type.id() == LogicalTypeId::INVALID) {
108+
throw InternalException("Return type not set in ScalarFunctionBuilder::AddVariant");
109+
}
110+
111+
// Add the new variant to the set
112+
set.AddFunction(std::move(builder.function));
113+
114+
// Add the default description if not set by the variant
115+
if (builder.description.description.empty()) {
116+
builder.description.description = default_description;
117+
}
118+
119+
// Add the description
120+
descriptions.emplace_back(std::move(builder.description));
121+
}
122+
123+
//------------------------------------------------------------------------------
124+
// Function Builder
125+
//------------------------------------------------------------------------------
126+
127+
class FunctionBuilder {
128+
public:
129+
template <class CALLBACK>
130+
static void RegisterScalar(DatabaseInstance &db, const char *name, CALLBACK &&callback);
131+
132+
private:
133+
static void Register(DatabaseInstance &db, const char *name, ScalarFunctionBuilder &builder);
134+
};
135+
136+
template <class CALLBACK>
137+
void FunctionBuilder::RegisterScalar(DatabaseInstance &db, const char *name, CALLBACK &&callback) {
138+
ScalarFunctionBuilder builder(name);
139+
callback(builder);
140+
141+
Register(db, name, builder);
142+
}
143+
144+
} // namespace core
145+
146+
} // namespace spatial

spatial/src/spatial/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ add_subdirectory(util)
66

77
set(EXTENSION_SOURCES
88
${EXTENSION_SOURCES}
9+
${CMAKE_CURRENT_SOURCE_DIR}/function_builder.cpp
910
${CMAKE_CURRENT_SOURCE_DIR}/module.cpp
1011
${CMAKE_CURRENT_SOURCE_DIR}/types.cpp
1112
${CMAKE_CURRENT_SOURCE_DIR}/optimizer_rules.cpp
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#include "spatial/core/function_builder.hpp"
2+
#include "duckdb/catalog/catalog_entry/function_entry.hpp"
3+
#include "duckdb/main/extension_util.hpp"
4+
5+
namespace spatial {
6+
7+
namespace core {
8+
9+
static string RemoveIndentAndTrailingWhitespace(const char *text) {
10+
string result;
11+
// Skip any empty first newlines if present
12+
while (*text == '\n') {
13+
text++;
14+
}
15+
16+
// Track indent length
17+
auto indent_start = text;
18+
while (isspace(*text) && *text != '\n') {
19+
text++;
20+
}
21+
auto indent_len = text - indent_start;
22+
while (*text) {
23+
result += *text;
24+
if (*text++ == '\n') {
25+
// Remove all indentation, but only if it matches the first line's indentation
26+
bool matched_indent = true;
27+
for (auto i = 0; i < indent_len; i++) {
28+
if (*text != indent_start[i]) {
29+
matched_indent = false;
30+
break;
31+
}
32+
}
33+
if (matched_indent) {
34+
text += indent_len;
35+
}
36+
}
37+
}
38+
39+
// Also remove any trailing whitespace
40+
result.erase(result.find_last_not_of(" \n\r\t") + 1);
41+
return result;
42+
}
43+
44+
void FunctionBuilder::Register(DatabaseInstance &db, const char *name, ScalarFunctionBuilder &builder) {
45+
// Register the function
46+
ExtensionUtil::RegisterFunction(db, std::move(builder.set));
47+
48+
// Also add the parameter names. We need to access the catalog entry for this.
49+
auto &catalog = Catalog::GetSystemCatalog(db);
50+
auto transaction = CatalogTransaction::GetSystemTransaction(db);
51+
auto &schema = catalog.GetSchema(transaction, DEFAULT_SCHEMA);
52+
auto catalog_entry = schema.GetEntry(transaction, CatalogType::SCALAR_FUNCTION_ENTRY, name);
53+
if (!catalog_entry) {
54+
// This should not happen, we just registered the function
55+
throw InternalException("Function with name \"%s\" not found in FunctionBuilder::AddScalar", name);
56+
}
57+
58+
auto &func_entry = catalog_entry->Cast<FunctionEntry>();
59+
60+
// Insert all descriptions
61+
for (auto &desc : builder.descriptions) {
62+
63+
desc.description = RemoveIndentAndTrailingWhitespace(desc.description.c_str());
64+
for (auto &ex : desc.examples) {
65+
ex = RemoveIndentAndTrailingWhitespace(ex.c_str());
66+
}
67+
68+
func_entry.descriptions.push_back(desc);
69+
}
70+
71+
if (!builder.tags.empty()) {
72+
func_entry.tags = std::move(builder.tags);
73+
}
74+
}
75+
76+
} // namespace core
77+
78+
} // namespace spatial

spatial/src/spatial/core/functions/scalar/st_contains.cpp

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#include "spatial/common.hpp"
2-
#include "spatial/core/types.hpp"
32
#include "spatial/core/functions/scalar.hpp"
3+
#include "spatial/core/types.hpp"
4+
#include "spatial/core/function_builder.hpp"
45

5-
#include "duckdb/parser/parsed_data/create_scalar_function_info.hpp"
66
namespace spatial {
77

88
namespace core {
@@ -148,18 +148,24 @@ static void PointWithinPolygonFunction(DataChunk &args, ExpressionState &state,
148148
//------------------------------------------------------------------------------
149149
void CoreScalarFunctions::RegisterStContains(DatabaseInstance &db) {
150150

151-
// ST_Within is the inverse of ST_Contains
152-
ScalarFunctionSet contains_function_set("ST_Contains");
153-
ScalarFunctionSet within_function_set("ST_Within");
151+
FunctionBuilder::RegisterScalar(db, "ST_Contains", [](ScalarFunctionBuilder &func) {
152+
func.AddVariant([](ScalarFunctionVariantBuilder &variant) {
153+
variant.AddParameter("geom1", GeoTypes::POLYGON_2D());
154+
variant.AddParameter("geom2", GeoTypes::POINT_2D());
155+
variant.SetReturnType(LogicalType::BOOLEAN);
156+
variant.SetFunction(PolygonContainsPointFunction);
157+
});
158+
});
154159

155-
// POLYGON_2D - POINT_2D
156-
contains_function_set.AddFunction(ScalarFunction({GeoTypes::POLYGON_2D(), GeoTypes::POINT_2D()},
157-
LogicalType::BOOLEAN, PolygonContainsPointFunction));
158-
within_function_set.AddFunction(ScalarFunction({GeoTypes::POINT_2D(), GeoTypes::POLYGON_2D()}, LogicalType::BOOLEAN,
159-
PointWithinPolygonFunction));
160-
161-
ExtensionUtil::RegisterFunction(db, contains_function_set);
162-
ExtensionUtil::RegisterFunction(db, within_function_set);
160+
// ST_Within is the inverse of ST_Contains
161+
FunctionBuilder::RegisterScalar(db, "ST_Within", [](ScalarFunctionBuilder &func) {
162+
func.AddVariant([](ScalarFunctionVariantBuilder &variant) {
163+
variant.AddParameter("geom1", GeoTypes::POINT_2D());
164+
variant.AddParameter("geom2", GeoTypes::POLYGON_2D());
165+
variant.SetReturnType(LogicalType::BOOLEAN);
166+
variant.SetFunction(PointWithinPolygonFunction);
167+
});
168+
});
163169
}
164170

165171
} // namespace core

0 commit comments

Comments
 (0)