A DuckDB extension that implements PostgreSQL-compatible range types and operations, enabling efficient storage and querying of value ranges.
This extension provides multiple range types with associated functions:
- INT4RANGE - Integer ranges (32-bit integers)
- NUMRANGE - Numeric ranges (double precision floating-point)
- Range Construction: Multiple constructors for creating ranges with different bound specifications
- Range Operators: Check for overlaps, containment with PostgreSQL-compatible
@>and<@operators - Range Accessors: Extract bounds and boundary inclusivity information
- Type Safety: Proper NULL handling and type casting
git clone --recurse-submodules https://github.com/abelcha/duckdb-ranges.git
cd duckdb-ranges
makeThe build produces:
./build/release/duckdb- DuckDB shell with the extension pre-loaded./build/release/extension/ranges/ranges.duckdb_extension- Loadable extension binary
LOAD 'build/release/extension/ranges/ranges.duckdb_extension';Or start the shell with the extension already loaded:
./build/release/duckdbThe int4range() function supports multiple signatures:
SELECT int4range('[1,10)') AS range;
-- Result: [1,10)
SELECT int4range(3, 10) AS range;
-- Result: [3, 10]Syntax: [lower,upper) where:
[or(indicates inclusive or exclusive lower bound]or)indicates inclusive or exc`lusive upper bound
SELECT int4range(1, 10) AS range;
-- Result: [1,10) (default is inclusive lower, exclusive upper)SELECT int4range(1, 10, '[]') AS range;
-- Result: [1,10]
SELECT int4range(1, 10, '()') AS range;
-- Result: (1,10)
SELECT int4range(1, 10, '(]') AS range;
-- Result: (1,10]SELECT int4range(1, 10, true, false) AS range;
-- Result: [1,10)
SELECT int4range(1, 10, false, true) AS range;
-- Result: (1,10]SELECT range_overlaps(int4range(1, 10), int4range(5, 15)) AS overlaps;
-- Result: true
SELECT range_overlaps(int4range(1, 5), int4range(10, 15)) AS overlaps;
-- Result: falseSELECT range_contains(int4range('[1,10)'), 5) AS contains;
-- Result: true
SELECT range_contains(int4range('[1,10)'), 10) AS contains;
-- Result: false (upper bound is exclusive)
SELECT range_contains(int4range('[1,10]'), 10) AS contains;
-- Result: true (upper bound is inclusive)SELECT isempty(int4range(5, 5)) AS is_empty;
-- Result: true
SELECT isempty(int4range('[5,5]')) AS is_empty;
-- Result: false (both bounds inclusive)SELECT lower(int4range('[1,10)')) AS lower_bound;
-- Result: 1SELECT upper(int4range('[1,10)')) AS upper_bound;
-- Result: 10SELECT lower_inc(int4range('[1,10)')) AS lower_inclusive;
-- Result: true
SELECT lower_inc(int4range('(1,10)')) AS lower_inclusive;
-- Result: falseSELECT upper_inc(int4range('[1,10)')) AS upper_inclusive;
-- Result: false
SELECT upper_inc(int4range('[1,10]')) AS upper_inclusive;
-- Result: trueRanges can be cast to/from VARCHAR for display and storage:
-- Implicit cast to VARCHAR for display
SELECT int4range(1, 10)::VARCHAR;
-- Result: '[1,10)'
-- Cast from VARCHAR to INT4RANGE
SELECT '[5,15)'::INT4RANGE;The NUMRANGE type works identically to INT4RANGE but uses double-precision floating-point numbers.
-- String literal
SELECT numrange('[0.0,1.0)') AS probability;
-- Result: [0.000000,1.000000)
-- Bounds with default notation [)
SELECT numrange(1.5, 10.5) AS range;
-- Result: [1.500000,10.500000)
-- Custom bound notation
SELECT numrange(0.0, 100.0, '[]') AS inclusive_range;
-- Result: [0.000000,100.000000]
-- Explicit inclusivity flags
SELECT numrange(-273.15, 100.0, true, false) AS temperature;
-- Result: [-273.150000,100.000000)All the same operations work with NUMRANGE:
-- Containment
SELECT numrange(0.0, 1.0) @> 0.5; -- true
SELECT 0.999 <@ numrange(0.0, 1.0); -- true
-- Overlaps
SELECT range_overlaps(numrange(1.5, 10.5), numrange(5.0, 15.0)); -- true
-- Accessors
SELECT lower(numrange(1.5, 10.5)); -- 1.5
SELECT upper(numrange(1.5, 10.5)); -- 10.5
SELECT isempty(numrange(5.0, 5.0)); -- trueCREATE TABLE products (
id INTEGER,
name VARCHAR,
price_range NUMRANGE
);
INSERT INTO products VALUES
(1, 'Budget Widget', numrange(9.99, 29.99)),
(2, 'Premium Widget', numrange(50.00, 150.00, '[]')),
(3, 'Luxury Widget', numrange(200.00, 500.00));
-- Find products available at $100
SELECT name, price_range
FROM products
WHERE price_range @> 100.0;
-- Result: Premium Widget
-- Find products under $50
SELECT name
FROM products
WHERE lower(price_range) < 50.0;
-- Results: Budget Widget, Premium Widget-- Temperature ranges in Celsius
SELECT
numrange(-273.15, 100.0, '[]') AS water_liquid_range,
numrange(-273.15, 100.0, '[]') @> 20.0 AS room_temp_is_liquid;
-- Probability ranges
SELECT
numrange(0.0, 1.0, '[)') AS probability_range,
0.75 <@ numrange(0.5, 1.0) AS high_probability;CREATE TABLE events (
id INTEGER,
name VARCHAR,
period INT4RANGE
);
INSERT INTO events VALUES
(1, 'Project A', int4range(2020, 2023)),
(2, 'Project B', int4range(2022, 2025)),
(3, 'Project C', int4range(2024, 2027));
-- Find all projects that overlap with 2022-2024
SELECT name
FROM events
WHERE range_overlaps(period, int4range(2022, 2024));
-- Results: Project A, Project B-- Find events active in year 2023
SELECT name
FROM events
WHERE range_contains(period, 2023);
-- Results: Project A, Project B-- Find ranges that include their upper bound
SELECT name, period
FROM events
WHERE upper_inc(period);- Storage: BLOB type with alias
INT4RANGE - Binary Format: 9 bytes (4 bytes lower + 4 bytes upper + 1 byte bounds flags)
- Value Range: -2,147,483,648 to 2,147,483,647 (32-bit signed integer)
- Empty Representation: Canonically
(1,0), displayed asempty
- Storage: BLOB type with alias
NUMRANGE - Binary Format: 17 bytes (8 bytes lower + 8 bytes upper + 1 byte bounds flags)
- Precision: Double-precision floating-point (IEEE 754)
- Empty Representation: Canonically
(1.0,0.0), displayed asempty
- Bounds Encoding: Single byte with bits for lower/upper inclusivity
- Null Handling: All functions properly handle NULL inputs
- Type Safety: Separate function overloads prevent type confusion
make testTests are located in test/sql/ and cover:
- Range construction with various notations
- Overlap detection
- Containment checks
- Accessor functions
- Edge cases (empty ranges, boundary conditions)
INT4RANGE- Integer range type (32-bit signed integers)NUMRANGE- Numeric range type (double-precision floating-point)
int4range(varchar)- Parse from string literalint4range(int, int)- Create with default bounds[)int4range(int, int, varchar)- Create with bound notation ([],[),(],())int4range(int, int, boolean, boolean)- Create with explicit inclusivity
numrange(varchar)- Parse from string literalnumrange(double, double)- Create with default bounds[)numrange(double, double, varchar)- Create with bound notation ([],[),(],())numrange(double, double, boolean, boolean)- Create with explicit inclusivity
Both range types support the following operators:
range_overlaps(RANGE, RANGE) -> BOOLEAN- Check if two ranges overlaprange_contains(RANGE, VALUE) -> BOOLEAN- Check if range contains a value
@>- Contains operator:range @> valueorINT4RANGE @> INTEGERorNUMRANGE @> DOUBLE<@- Contained by operator:value <@ rangeorINTEGER <@ INT4RANGEorDOUBLE <@ NUMRANGE
The following functions work with both INT4RANGE and NUMRANGE:
lower(RANGE)- Extract lower bound (returns INTEGER for INT4RANGE, DOUBLE for NUMRANGE)upper(RANGE)- Extract upper bound (returns INTEGER for INT4RANGE, DOUBLE for NUMRANGE)lower_inc(RANGE) -> BOOLEAN- Check if lower bound is inclusiveupper_inc(RANGE) -> BOOLEAN- Check if upper bound is inclusiveisempty(RANGE) -> BOOLEAN- Check if range is empty
See LICENSE file for details.
This extension is based on the DuckDB Extension Template.
To contribute:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass with
make test - Submit a pull request