Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 68 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ It currently provides:
- `clip(value, min, max)` - alias for `clamp`
- `saturate(value)` — clamp to the `[0, 1]` range
- `clamp01(value)` — alias for `saturate`, common in graphics and ML
- `wrap(value, min, max)` - Wrap a value x into the range [min_val, max_val) using modular arithmetic.
- `wrap(value, min, max)` - Wrap a value x into the range [min_val, max_val) using modular arithmetic
- `pingpong(value, min, max)` - Return a value that "bounces" back and forth between the minimum and maximum
- `fract(value)` - Return the fractional (decimal) part of a number

All functions are implemented as native DuckDB scalar functions with
vectorized execution for high performance.
Expand All @@ -30,16 +32,17 @@ vectorized execution for high performance.

- Native DuckDB scalar functions (`ScalarFunctionSet`)
- Vectorized execution via DuckDB executors
- `TernaryExecutor` for `clamp` / `clip` / `wrap`
- `UnaryExecutor` for `saturate` / `clamp01`
- `TernaryExecutor` for `clamp` / `clip` / `wrap` / `pingpong`
- `UnaryExecutor` for `saturate` / `clamp01` / `fract`
- Supports:
- `BIGINT` (`int64_t`) — `clamp`, `saturate1`, `clamp01`, `clip`, `wrap`
- `DOUBLE` — `clamp`, `saturate`, `clamp01`, `clip`, `wrap`
- `BIGINT` (`int64_t`) — `clamp`, `saturate1`, `clamp01`, `clip`, `wrap`, `pingpong`, `fract`
- `DOUBLE` — `clamp`, `saturate`, `clamp01`, `clip`, `wrap`, `pingpong`, `fract`
- NULL-safe:
- returns `NULL` if any input argument is `NULL`
- Strict validation:
- `clamp` / `clip` throws an error if `min > max`
- `wrap` throws an error if `min >= max`
- `pingpong` throws an error if `min>=max`

---

Expand Down Expand Up @@ -192,6 +195,9 @@ Definition:
WRAP(x, min_val, max_val) = min_val + ((x - min_val) % (max_val - min_val))

```sql
SELECT wrap(11, 0, 10);
-- 1

SELECT wrap(25, 10, 20);
--15

Expand All @@ -207,6 +213,58 @@ SELECT wrap(-15, 10, 20);
SELECT wrap(370, 0, 360), wrap(-10, 0, 360), wrap(720, 0, 360);

-- 10.0 350.0 0.0
```
---
### pingpong(value, min, max)

Returns a value that "bounces" back and forth between the minimum and maximum
boundaries. This creates a triangle wave pattern, commonly used for smooth
oscillations in animations and procedural generation.

When `value` is at `min`, the result is `min`. As `value` increases toward
`max`, the result increases linearly. Upon hitting `max`, the result reverses
direction and decreases back toward `min`. The output is periodic with a full
"round-trip" period of 2 * (max-min).

```sql
SELECT pingpong(11, 0, 10);
-- 9

-- Oscillation between 10 and 20
SELECT pingpong(12, 10, 20);
-- 12.0

-- 2 past the max (20), so it bounces back to 18
SELECT pingpong(22, 10, 20);
-- 18.0

-- 8 past the max (20), so it bounces back to 12
SELECT pingpong(28, 10, 20);
-- 12.0
```
---
### fract(val)

Returns the fractional (decimal) part of a number. This is defined as
x - floor(x).

Isolates the digits following the decimal point. For negative numbers,
`fract` returns the positive remainder required to reach the next
lower integer.

```sql
-- Standard use
SELECT fract(1.75);
-- 0.75

-- Integer inputs always return 0
SELECT fract(10);
-- 0.0

-- Negative wrapping (Blender/Shader style)
SELECT fract(-0.1);
-- 0.9

```
---

Expand All @@ -218,6 +276,8 @@ clip(value, min, max)
saturate(value)
clamp01(value)
wrap(value, min, max)
pingpong(value, min, max)
fract(value)
```

### Parameters
Expand All @@ -244,6 +304,8 @@ All arguments must be of the same type.
| clamp01 | ✓ | ✓ |
| clip | ✓ | ✓ |
| wrap | ✓ | ✓ |
| pingpong | ✓ | ✓ |
| fract | ✓ | ✓ |

---

Expand Down Expand Up @@ -278,22 +340,12 @@ make test
## Potential Future Additions

While this extension currently provides `clamp`, `clip`, `wrap`, `saturate`, and
`clamp01`, it intentionally leaves room for other mathematically
others, it intentionally leaves room for other mathematically
range-based utilities that frequently appear in numerical computing,
analytics, and graphics domains.

Possible future additions include:

- **Ping-Pong**
Reflects values back and forth between bounds, useful for oscillating
ranges or bounded waveforms.

- **Lerp (Linear Interpolation)**
Interpolates between two values using a normalized parameter.

- **Fract**
Returns the fractional (decimal) part of a floating-point number.

- **IsPowerOfTwo**
Boolean check that returns `true` if a number is a power of two.

Expand Down
2 changes: 1 addition & 1 deletion duckdb
Submodule duckdb updated 4390 files
2 changes: 1 addition & 1 deletion extension-ci-tools
Submodule extension-ci-tools updated 35 files
+30 −10 .github/workflows/TestCITools.yml
+17 −10 .github/workflows/_extension_deploy.yml
+41 −43 .github/workflows/_extension_distribution.yml
+4 −1 README.md
+17 −9 config/distribution_matrix.json
+14 −0 docker/linux_amd64/Dockerfile
+14 −5 docker/linux_amd64_musl/Dockerfile
+14 −0 docker/linux_arm64/Dockerfile
+109 −0 docker/linux_arm64_musl/Dockerfile
+3 −7 makefiles/c_api_extensions/base.Makefile
+0 −6 makefiles/duckdb_extension.Makefile
+10 −0 makefiles/vcpkg.Makefile
+2 −0 scripts/extbuild/.gitignore
+21 −0 scripts/extbuild/Makefile
+34 −0 scripts/extbuild/README.md
+48 −0 scripts/extbuild/cmd/extbuild/github_event.go
+141 −0 scripts/extbuild/cmd/extbuild/github_event_test.go
+132 −0 scripts/extbuild/cmd/extbuild/logging.go
+13 −0 scripts/extbuild/cmd/extbuild/main.go
+112 −0 scripts/extbuild/cmd/extbuild/matrix.go
+322 −0 scripts/extbuild/cmd/extbuild/matrix_test.go
+19 −0 scripts/extbuild/cmd/extbuild/root.go
+16 −0 scripts/extbuild/go.mod
+18 −0 scripts/extbuild/go.sum
+55 −0 scripts/extbuild/internal/distmatrix/config_test.go
+252 −0 scripts/extbuild/internal/distmatrix/matrix.go
+102 −0 scripts/extbuild/internal/distmatrix/matrix_test.go
+101 −0 scripts/extbuild/internal/distmatrix/output.go
+12 −0 scripts/extbuild/testdata/github/events/extension_template_pull_request.json
+8 −0 scripts/extbuild/testdata/github/events/extension_template_push.json
+6 −0 scripts/extbuild/testdata/github/events/extension_template_unknown.json
+0 −102 scripts/modify_distribution_matrix.py
+0 −11 toolchains/arm64-windows-static-md-release-vs2019comp.cmake
+0 −11 toolchains/x64-windows-static-md-release-vs2019comp.cmake
+1 −1 vcpkg_ports/vcpkg-cmake/vcpkg.json
188 changes: 188 additions & 0 deletions src/clamp_extension.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
#include "duckdb.hpp"
#include "duckdb/function/scalar_function.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <type_traits>

namespace duckdb {

Expand Down Expand Up @@ -139,6 +142,147 @@ static void WrapFunction(DataChunk &args, ExpressionState &state, Vector &result
WrapOperator::Operation<T>);
}

//------------------------------------------------------------------------------
// FractOperator: Decimal part of a number (x - floor(x))
//------------------------------------------------------------------------------
// Helper for floating point types (double, float)
template <class T>
static inline typename std::enable_if<std::is_floating_point<T>::value, T>::type FractLogic(T a) {
return a - std::floor(a);
}

// Helper for non-floating point types (int64_t, etc.)
template <class T>
static inline typename std::enable_if<!std::is_floating_point<T>::value, T>::type FractLogic(T a) {
return 0;
}

struct FractOperator {
template <class T>
static inline T Operation(T a) {
// No NaNs in integers, but let the compiler handle the check for floats
if (std::is_floating_point<T>::value) {
if (std::isnan(static_cast<double>(a))) {
return std::numeric_limits<T>::quiet_NaN();
}
}

return FractLogic<T>(a);
}
};

//------------------------------------------------------------------------------
// FractFunction: DuckDB executor wrapper for FractOperator
//------------------------------------------------------------------------------
template <class T>
static void FractFunction(DataChunk &args, ExpressionState &state, Vector &result) {
UnaryExecutor::Execute<T, T>(args.data[0], result, args.size(), FractOperator::Operation<T>);
}

//------------------------------------------------------------------------------
// PingPongOperator: Triangle Wave Generator
//------------------------------------------------------------------------------
// Based on the Blender/GLSL math logic. It creates a continuous oscillation
// between min_val and max_val. As 'val' increases, the result moves from
// min to max, then reverses back to min.
//------------------------------------------------------------------------------

// Helper for Integers
// Uses integer division and modulus to create a triangle wave pattern
template <class T>
static inline typename std::enable_if<std::is_integral<T>::value, T>::type PingPongLogic(T val, T min_val, T max_val) {
// Calculate range. We use uint64_t for the range to safely
// handle the case where max is max_int and min is min_int.
uint64_t u_range = static_cast<uint64_t>(max_val) - static_cast<uint64_t>(min_val);
if (u_range == 0)
return min_val;

// Calculate offset. We use __int128 if available for absolute safety,
// but for standard BIGINT, we can use careful logic with the range.
int64_t offset = static_cast<int64_t>(val) - static_cast<int64_t>(min_val);

// Euclidean Division: Calculate quotient and remainder such that
// remainder is always in [0, u_range).
int64_t q = offset / static_cast<int64_t>(u_range);
int64_t r = offset % static_cast<int64_t>(u_range);

// Adjust for negative offsets to ensure a continuous wave across the origin
if (r < 0) {
r += u_range;
q -= 1;
}

// Parity Check (The Bounce)
// If the quotient is even (0, 2, -2...), we are moving UP from min.
// If the quotient is odd (1, 3, -1, -3...), we are moving DOWN from max.
if (std::abs(q) % 2 == 0) {
return static_cast<T>(static_cast<uint64_t>(min_val) + r);
} else {
return static_cast<T>(static_cast<uint64_t>(max_val) - r);
}
}

// Helper for Floating Point
// Uses fmod and floating point arithmetic to create a triangle wave pattern
template <class T>
static inline typename std::enable_if<std::is_floating_point<T>::value, T>::type PingPongLogic(T val, T min_val,
T max_val) {
double d_val = static_cast<double>(val);
double d_min = static_cast<double>(min_val);
double d_max = static_cast<double>(max_val);

double range = d_max - d_min;
double period = range * 2.0;

// Use fmod for doubles instead of %
double t = std::fmod(d_val - d_min, period);
if (t < 0)
t += period;

if (t > range) {
return static_cast<T>(d_min + (period - t));
}
return static_cast<T>(d_min + t);
}

struct PingPongOperator {
template <class T>
static inline T Operation(T val, T min_val, T max_val) {
// Use standard is_floating_point<T>::value for C++11 compatibility
if (std::is_floating_point<T>::value) {
if (std::isnan(static_cast<double>(val)) || std::isnan(static_cast<double>(min_val)) ||
std::isnan(static_cast<double>(max_val))) {
return std::numeric_limits<T>::quiet_NaN();
}
}

// Validate bounds: min_val must not be greater than or equal to max_val
if (min_val >= max_val) {
throw InvalidInputException(
"Error: Minimum bound (%s) cannot be greater than or equal to maximum bound (%s).",
std::to_string(min_val), std::to_string(max_val));
}

// The compiler picks the correct PingPongLogic overload at compile time
return PingPongLogic<T>(val, min_val, max_val);
}
};

//------------------------------------------------------------------------------
// PingPongFunction: DuckDB executor wrapper for PingPongOperator
//------------------------------------------------------------------------------
template <class T>
static void PingPongFunction(DataChunk &args, ExpressionState &state, Vector &result) {
// Uses DuckDB's TernaryExecutor to apply PingPongOperator::Operation to each row
// args.data[0]: value to pingpong
// args.data[1]: minimum bound
// args.data[2]: maximum bound
// result: output vector
// args.size(): number of rows
TernaryExecutor::Execute<T, T, T, T>(args.data[0], args.data[1], args.data[2], result, args.size(),
PingPongOperator::Operation<T>);
}

//------------------------------------------------------------------------------
// LoadInternal: Registers the clamp function(s) with DuckDB
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -210,12 +354,56 @@ static void LoadInternal(ExtensionLoader &loader) {
wrap.AddFunction(double_wrap_fun);
wrap.AddFunction(bigint_wrap_fun);

// ------------------------------------------------------------------------------
// PINGPONG
// ------------------------------------------------------------------------------
ScalarFunctionSet pingpong("pingpong");

// Define pingpong for DOUBLE type
auto double_pingpong_fun =
ScalarFunction({LogicalType::DOUBLE, LogicalType::DOUBLE, LogicalType::DOUBLE}, // argument types
LogicalType::DOUBLE, // return type
PingPongFunction<double> // implementation
);
// Specify null handling: returns NULL if any input is NULL
double_pingpong_fun.null_handling = FunctionNullHandling::DEFAULT_NULL_HANDLING;

// Define pingpong for BIGINT (int64_t) type
auto bigint_pingpong_fun = ScalarFunction({LogicalType::BIGINT, LogicalType::BIGINT, LogicalType::BIGINT},
LogicalType::BIGINT, PingPongFunction<int64_t>);
bigint_pingpong_fun.null_handling = FunctionNullHandling::DEFAULT_NULL_HANDLING;

// Add both type-specific implementations to the function set
pingpong.AddFunction(double_pingpong_fun);
pingpong.AddFunction(bigint_pingpong_fun);

// ------------------------------------------------------------------------------
// FRACT
// ------------------------------------------------------------------------------
ScalarFunctionSet fract("fract");

// Define fract for DOUBLE type
auto double_fract_fun = ScalarFunction({LogicalType::DOUBLE}, LogicalType::DOUBLE, FractFunction<double>);
double_fract_fun.null_handling = FunctionNullHandling::DEFAULT_NULL_HANDLING;
fract.AddFunction(double_fract_fun);

// Define fract for BIGINT (int64_t) type
auto bigint_fract_fun = ScalarFunction({LogicalType::BIGINT}, LogicalType::BIGINT, FractFunction<int64_t>);
bigint_fract_fun.null_handling = FunctionNullHandling::DEFAULT_NULL_HANDLING;
fract.AddFunction(bigint_fract_fun);

// ------------------------------------------------------------------------------
// REGISTER FUNCTIONS
// ------------------------------------------------------------------------------

// Register the function set with DuckDB
loader.RegisterFunction(clamp);
loader.RegisterFunction(clamp_alias);
loader.RegisterFunction(saturate);
loader.RegisterFunction(saturate_alias);
loader.RegisterFunction(wrap);
loader.RegisterFunction(pingpong);
loader.RegisterFunction(fract);
}

//------------------------------------------------------------------------------
Expand Down
Loading
Loading