From 62d023d6bff8c1ec9304e793306f4f2b4b639d4c Mon Sep 17 00:00:00 2001 From: Tushar Malpani Date: Mon, 3 Aug 2026 23:36:03 +0530 Subject: [PATCH 1/3] test: cover rational edge cases --- expected/pg_rational_test.out | 36 +++++++++++++++++++++++++++++++++++ sql/pg_rational_test.sql | 20 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/expected/pg_rational_test.out b/expected/pg_rational_test.out index 73ba945..e06051d 100644 --- a/expected/pg_rational_test.out +++ b/expected/pg_rational_test.out @@ -305,6 +305,11 @@ select '2/3'::rational / '2/3'; 6/6 (1 row) +-- division by zero + +select '1/2'::rational / 0; +ERROR: division by zero + -- negation -- flips sign of numerator select -('1/2'::rational); @@ -461,6 +466,26 @@ select '1/2147483647'::rational < '2/2147483647'; t (1 row) +-- negative denominators created through the constructor compare by value + +select rational_create(1, -2) = '-1/2'::rational; + ?column? +---------- + t +(1 row) + +select rational_create(1, -2) < 0; + ?column? +---------- + t +(1 row) + +select rational_create(1, -2) > '-1/1'::rational; + ?column? +---------- + t +(1 row) + -- lte select r from unnest(ARRAY[ @@ -688,3 +713,14 @@ select rational_intermediate(NULL, '15/16'); 1/2 (1 row) +-- bounded intermediate search + +select rational_intermediate(0, '1/2147483647', 1); +ERROR: maximum search depth exceeded + +select rational_intermediate( + '1836311903/1134903170', + '1134903170/701408733', + 100 +); +ERROR: intermediate value overflow in rational intermediate diff --git a/sql/pg_rational_test.sql b/sql/pg_rational_test.sql index c6359db..23b7330 100644 --- a/sql/pg_rational_test.sql +++ b/sql/pg_rational_test.sql @@ -103,6 +103,10 @@ select '46342/46341'::rational * '46341/46342'; select 1::rational / 3; select '2/3'::rational / '2/3'; +-- division by zero + +select '1/2'::rational / 0; + -- negation -- flips sign of numerator @@ -156,6 +160,12 @@ select '1/2'::rational > '1/2'; -- overflow not possible select '1/2147483647'::rational < '2/2147483647'; +-- negative denominators created through the constructor compare by value + +select rational_create(1, -2) = '-1/2'::rational; +select rational_create(1, -2) < 0; +select rational_create(1, -2) > '-1/1'::rational; + -- lte select r from unnest(ARRAY[ @@ -269,3 +279,13 @@ select rational_intermediate('1/3', NULL); select rational_intermediate('3/2', NULL); -- though not the other direction select rational_intermediate(NULL, '15/16'); + +-- bounded intermediate search + +select rational_intermediate(0, '1/2147483647', 1); + +select rational_intermediate( + '1836311903/1134903170', + '1134903170/701408733', + 100 +); From 49a65a511fa96acc72c589990df824eeba6ef2b7 Mon Sep 17 00:00:00 2001 From: Tushar Malpani Date: Mon, 3 Aug 2026 23:36:40 +0530 Subject: [PATCH 2/3] fix: guard rational edge cases --- Makefile | 2 +- README.md | 6 +- pg_rational--0.0.3--0.0.4.sql | 4 + pg_rational--0.0.4.sql | 359 ++++++++++++++++++++++++++++++++++ pg_rational.c | 61 +++++- pg_rational.control | 2 +- 6 files changed, 423 insertions(+), 11 deletions(-) create mode 100644 pg_rational--0.0.3--0.0.4.sql create mode 100644 pg_rational--0.0.4.sql diff --git a/Makefile b/Makefile index f001e97..d3bf888 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ MODULES = pg_rational EXTENSION = pg_rational -DATA = pg_rational--0.0.1.sql pg_rational--0.0.1--0.0.2.sql pg_rational--0.0.2.sql pg_rational--0.0.2--0.0.3.sql pg_rational--0.0.3.sql +DATA = pg_rational--0.0.1.sql pg_rational--0.0.1--0.0.2.sql pg_rational--0.0.2.sql pg_rational--0.0.2--0.0.3.sql pg_rational--0.0.3.sql pg_rational--0.0.3--0.0.4.sql pg_rational--0.0.4.sql REGRESS = pg_rational_test PG_CPPFLAGS = -std=c11 -Wextra -Wpedantic diff --git a/README.md b/README.md index 674a565..2c9c0de 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ precision in the denominator. * Defers GCD calculation until requested or absolutely required * Supports btree and hash indices * Implements Stern-Brocot trees for finding intermediate points -* Coercion from integer/bigint/tuple +* Coercion from integer/float/tuple * Custom aggregate ### Motivation @@ -120,7 +120,7 @@ select * from todos order by prio asc; */ ``` -This extension uses Stern-Brocot trees to find efficient intermediate points as fractions in lowest terms. It can continue to split deeper between fractions as much as any practical application requires. +This extension uses Stern-Brocot trees to find efficient intermediate points as fractions in lowest terms. The two-argument form limits the search to one million steps; use the three-argument form to choose a different positive limit when needed. Using floats, on the other hand, and picking the midpoints between adjacent values runs out of space rapidly (you only need 50-odd inserts at the wrong spot to start hitting problems). @@ -141,7 +141,7 @@ create extension pg_rational; ### Caveats -The `rational_intermediate` function is super fast on typical intervals, but the narrower the range between arguments the longer it takes. We may want to add a max search depth parameter to prevent malicious values from hogging the server. +The `rational_intermediate` function is super fast on typical intervals, but the narrower the range between arguments the longer it takes. The optional third argument sets the maximum search depth and prevents unusually narrow intervals from using the server indefinitely. ### Thanks diff --git a/pg_rational--0.0.3--0.0.4.sql b/pg_rational--0.0.3--0.0.4.sql new file mode 100644 index 0000000..72cde50 --- /dev/null +++ b/pg_rational--0.0.3--0.0.4.sql @@ -0,0 +1,4 @@ +CREATE FUNCTION rational_intermediate(rational, rational, integer) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE; diff --git a/pg_rational--0.0.4.sql b/pg_rational--0.0.4.sql new file mode 100644 index 0000000..122aeae --- /dev/null +++ b/pg_rational--0.0.4.sql @@ -0,0 +1,359 @@ +\echo Use "CREATE EXTENSION pg_rational" to load this file. \quit + +CREATE FUNCTION rational_in(cstring) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION rational_in_float(float8) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION rational_out(rational) +RETURNS cstring +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION rational_out_float(rational) +RETURNS float8 +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION rational_recv(internal) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION rational_send(rational) +RETURNS bytea +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE TYPE rational ( + INPUT = rational_in, + OUTPUT = rational_out, + RECEIVE = rational_recv, + SEND = rational_send, + INTERNALLENGTH = 8 +); + +CREATE FUNCTION rational_create(integer, integer) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE TYPE ratt AS (n integer, d integer); +CREATE FUNCTION tuple_to_rational(ratt) +RETURNS rational AS $$ + SELECT rational_create($1.n,$1.d); +$$ LANGUAGE SQL; + +CREATE CAST (ratt AS rational) + WITH FUNCTION tuple_to_rational(ratt) + AS IMPLICIT; + +CREATE CAST (float8 AS rational) + WITH FUNCTION rational_in_float(float8) + AS IMPLICIT; + +CREATE CAST (rational as float8) + WITH FUNCTION rational_out_float(rational) + AS IMPLICIT; + +CREATE FUNCTION rational_embed(integer) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE CAST (integer AS rational) + WITH FUNCTION rational_embed(integer) + AS IMPLICIT; + +CREATE FUNCTION rational_add(rational, rational) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR + ( + leftarg = rational, + rightarg = rational, + procedure = rational_add, + commutator = + +); + +CREATE FUNCTION rational_sub(rational, rational) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR - ( + leftarg = rational, + rightarg = rational, + procedure = rational_sub +); + +CREATE FUNCTION rational_mul(rational, rational) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR * ( + leftarg = rational, + rightarg = rational, + procedure = rational_mul, + commutator = * +); + +CREATE FUNCTION rational_div(rational, rational) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR / ( + leftarg = rational, + rightarg = rational, + procedure = rational_div +); + +CREATE FUNCTION rational_neg(rational) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR - ( + rightarg = rational, + procedure = rational_neg +); + +CREATE FUNCTION rational_simplify(rational) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION rational_intermediate(rational, rational) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE; + +CREATE FUNCTION rational_intermediate(rational, rational, integer) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE; + +------------- Comparison ------------- + +CREATE FUNCTION rational_eq(rational, rational) +RETURNS boolean +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR = ( + LEFTARG = rational, + RIGHTARG = rational, + PROCEDURE = rational_eq, + COMMUTATOR = '=', + NEGATOR = '<>', + RESTRICT = eqsel, + JOIN = eqjoinsel, + HASHES, MERGES +); + +CREATE FUNCTION rational_ne(rational, rational) +RETURNS boolean +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR <> ( + LEFTARG = rational, + RIGHTARG = rational, + PROCEDURE = rational_ne, + COMMUTATOR = '<>', + NEGATOR = '=', + RESTRICT = neqsel, + JOIN = neqjoinsel +); + +CREATE FUNCTION rational_lt(rational, rational) +RETURNS boolean +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR < ( + LEFTARG = rational, + RIGHTARG = rational, + PROCEDURE = rational_lt, + COMMUTATOR = > , + NEGATOR = >= , + RESTRICT = scalarltsel, + JOIN = scalarltjoinsel +); + +CREATE FUNCTION rational_le(rational, rational) +RETURNS boolean +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR <= ( + LEFTARG = rational, + RIGHTARG = rational, + PROCEDURE = rational_le, + COMMUTATOR = >= , + NEGATOR = > , + RESTRICT = scalarltsel, + JOIN = scalarltjoinsel +); + +CREATE FUNCTION rational_gt(rational, rational) +RETURNS boolean +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR > ( + LEFTARG = rational, + RIGHTARG = rational, + PROCEDURE = rational_gt, + COMMUTATOR = < , + NEGATOR = <= , + RESTRICT = scalargtsel, + JOIN = scalargtjoinsel +); + +CREATE FUNCTION rational_ge(rational, rational) +RETURNS boolean +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR >= ( + LEFTARG = rational, + RIGHTARG = rational, + PROCEDURE = rational_ge, + COMMUTATOR = <= , + NEGATOR = < , + RESTRICT = scalargtsel, + JOIN = scalargtjoinsel +); + +CREATE FUNCTION rational_cmp(rational, rational) +RETURNS integer +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR CLASS btree_rational_ops +DEFAULT FOR TYPE rational USING btree +AS + OPERATOR 1 < , + OPERATOR 2 <= , + OPERATOR 3 = , + OPERATOR 4 >= , + OPERATOR 5 > , + FUNCTION 1 rational_cmp(rational, rational); + +CREATE FUNCTION rational_hash(rational) +RETURNS integer +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OPERATOR CLASS hash_rational_ops + DEFAULT FOR TYPE rational USING hash AS + OPERATOR 1 = , + FUNCTION 1 rational_hash(rational); + + +------------- Aggregates ------------- + +CREATE FUNCTION rational_smaller(rational, rational) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION rational_larger(rational, rational) +RETURNS rational +AS '$libdir/pg_rational' +LANGUAGE C IMMUTABLE STRICT; + +------- Parallel-safe optimization -------- + +DO LANGUAGE plpgsql $$ +BEGIN + IF current_setting('server_version_num')::int >= 90600 + THEN + -- newish pg + + EXECUTE $alter$ + ALTER FUNCTION rational_in(cstring) PARALLEL SAFE; + ALTER FUNCTION rational_in_float(float8) PARALLEL SAFE; + ALTER FUNCTION rational_out(rational) PARALLEL SAFE; + ALTER FUNCTION rational_out_float(rational) PARALLEL SAFE; + ALTER FUNCTION rational_recv(internal) PARALLEL SAFE; + ALTER FUNCTION rational_send(rational) PARALLEL SAFE; + ALTER FUNCTION rational_create(integer, integer) PARALLEL SAFE; + ALTER FUNCTION rational_embed(integer) PARALLEL SAFE; + ALTER FUNCTION rational_add(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_sub(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_mul(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_div(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_neg(rational) PARALLEL SAFE; + ALTER FUNCTION rational_simplify(rational) PARALLEL SAFE; + ALTER FUNCTION rational_eq(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_ne(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_lt(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_le(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_gt(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_ge(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_cmp(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_smaller(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_larger(rational, rational) PARALLEL SAFE; + $alter$; + + EXECUTE $agg$ + CREATE AGGREGATE min(rational) ( + SFUNC = rational_smaller, + STYPE = rational, + SORTOP = <, + COMBINEFUNC = rational_smaller, + PARALLEL = SAFE + ); + + CREATE AGGREGATE max(rational) ( + SFUNC = rational_larger, + STYPE = rational, + SORTOP = >, + COMBINEFUNC = rational_larger, + PARALLEL = SAFE + ); + + CREATE AGGREGATE sum (rational) + ( + SFUNC = rational_add, + STYPE = rational, + COMBINEFUNC = rational_add, + PARALLEL = SAFE + ); + $agg$; + + ELSE + -- old pg + + EXECUTE $agg$ + CREATE AGGREGATE min(rational) ( + SFUNC = rational_smaller, + STYPE = rational, + SORTOP = < + ); + + CREATE AGGREGATE max(rational) ( + SFUNC = rational_larger, + STYPE = rational, + SORTOP = > + ); + + CREATE AGGREGATE sum (rational) + ( + SFUNC = rational_add, + STYPE = rational + ); + $agg$; + END IF; +END +$$; diff --git a/pg_rational.c b/pg_rational.c index 4057b5a..8a5351f 100644 --- a/pg_rational.c +++ b/pg_rational.c @@ -10,6 +10,8 @@ #include #include +#define RATIONAL_INTERMEDIATE_DEFAULT_MAX_STEPS 1000000 + PG_MODULE_MAGIC; typedef struct @@ -307,6 +309,12 @@ rational_div(PG_FUNCTION_ARGS) memcpy(&x, PG_GETARG_POINTER(0), sizeof(Rational)); memcpy(&y, PG_GETARG_POINTER(1), sizeof(Rational)); + + if (y.numer == 0) + ereport(ERROR, + (errcode(ERRCODE_DIVISION_BY_ZERO), + errmsg("division by zero"))); + tmp = y.numer; y.numer = y.denom; y.denom = tmp; @@ -356,6 +364,21 @@ rational_intermediate(PG_FUNCTION_ARGS) lo = {0, 1}, hi = {1, 0}, /* yes, an internal use of 1/0 */ *med = palloc(sizeof(Rational)); + int32 max_steps = RATIONAL_INTERMEDIATE_DEFAULT_MAX_STEPS, + steps = 0; + + if (PG_NARGS() == 3) + { + if (PG_ARGISNULL(2)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("maximum search depth cannot be null"))); + max_steps = PG_GETARG_INT32(2); + } + if (max_steps <= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("maximum search depth must be positive"))); /* * x = coalesce(lo, arg[0]) y = coalesce(hi, arg[1]) @@ -379,6 +402,11 @@ rational_intermediate(PG_FUNCTION_ARGS) while (true) { + if (steps >= max_steps) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("maximum search depth exceeded"))); + steps++; mediant(&lo, &hi, med); if (cmp(med, &x) < 1) memcpy(&lo, med, sizeof(Rational)); @@ -562,8 +590,26 @@ cmp(Rational * a, Rational * b) * Overflow is not an option, we need a total order so that btree indices * do not die. Hence do the arithmetic in 64 bits. */ - int64 cross1 = (int64) a->numer * (int64) b->denom, - cross2 = (int64) a->denom * (int64) b->numer; + int64 an = a->numer, + ad = a->denom, + bn = b->numer, + bd = b->denom, + cross1, + cross2; + + if (ad < 0) + { + an = -an; + ad = -ad; + } + if (bd < 0) + { + bn = -bn; + bd = -bd; + } + + cross1 = an * bd; + cross2 = ad * bn; return (cross1 > cross2) - (cross1 < cross2); } @@ -668,9 +714,12 @@ mediant(Rational * x, Rational * y, Rational * m) { /* * Rational_intermediate sends fractions with small numers and denoms, and - * slowly builds up. The search will take forever before we ever get close - * to arithmetic overflow in this function, so I don't guard it here. + * slowly builds up. Guard the additions anyway so unusual inputs fail + * cleanly instead of invoking signed-integer overflow. */ - m->numer = x->numer + y->numer; - m->denom = x->denom + y->denom; + if (pg_add_s32_overflow(x->numer, y->numer, &m->numer) || + pg_add_s32_overflow(x->denom, y->denom, &m->denom)) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("intermediate value overflow in rational intermediate"))); } diff --git a/pg_rational.control b/pg_rational.control index 5ce0401..f89514e 100644 --- a/pg_rational.control +++ b/pg_rational.control @@ -1,2 +1,2 @@ comment = 'integer fractions' -default_version = '0.0.3' +default_version = '0.0.4' From eac05eb2837ef7046e3859e474376c0116fc4393 Mon Sep 17 00:00:00 2001 From: Tushar Malpani Date: Mon, 3 Aug 2026 23:47:17 +0530 Subject: [PATCH 3/3] fix: harden numeric edge cases --- expected/pg_rational_test.out | 29 +++++++++++++++++++++++++++++ pg_rational--0.0.3--0.0.4.sql | 12 ++++++++++++ pg_rational--0.0.4.sql | 2 ++ pg_rational.c | 29 +++++++++++++++-------------- sql/pg_rational_test.sql | 13 +++++++++++++ 5 files changed, 71 insertions(+), 14 deletions(-) diff --git a/expected/pg_rational_test.out b/expected/pg_rational_test.out index e06051d..a82164b 100644 --- a/expected/pg_rational_test.out +++ b/expected/pg_rational_test.out @@ -108,6 +108,10 @@ select 2147483647.1::float::rational; ERROR: value too large for rational select 'NAN'::float::rational; ERROR: value too large for rational +select 'Infinity'::float::rational; +ERROR: value too large for rational +select '-Infinity'::float::rational; +ERROR: value too large for rational -- to float select '1/2'::rational::float; float8 @@ -207,6 +211,20 @@ select rational_simplify('1/-2147483648'); 1/-2147483648 (1 row) +-- minimum numerator with a negative unit denominator remains representable +select rational_simplify(rational_create(-2147483648, -1)); + rational_simplify +------------------- + -2147483648/-1 +(1 row) + +-- zero with the minimum denominator has the same hash as zero over one +select rational_hash(rational_create(0, -2147483648)) = rational_hash('0/1'::rational); + ?column? +---------- + t +(1 row) + -- biggest value reduces select rational_simplify('2147483647/2147483647'); rational_simplify @@ -724,3 +742,14 @@ select rational_intermediate( 100 ); ERROR: intermediate value overflow in rational intermediate + +-- bounded intermediate search is parallel safe + +select proparallel + from pg_proc + where proname = 'rational_intermediate' + and pronargs = 3; + proparallel +------------- + s +(1 row) diff --git a/pg_rational--0.0.3--0.0.4.sql b/pg_rational--0.0.3--0.0.4.sql index 72cde50..e2da7e6 100644 --- a/pg_rational--0.0.3--0.0.4.sql +++ b/pg_rational--0.0.3--0.0.4.sql @@ -2,3 +2,15 @@ CREATE FUNCTION rational_intermediate(rational, rational, integer) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE; + +DO LANGUAGE plpgsql $$ +BEGIN + IF current_setting('server_version_num')::int >= 90600 + THEN + EXECUTE $alter$ + ALTER FUNCTION rational_intermediate(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_intermediate(rational, rational, integer) PARALLEL SAFE; + $alter$; + END IF; +END +$$; diff --git a/pg_rational--0.0.4.sql b/pg_rational--0.0.4.sql index 122aeae..eec5720 100644 --- a/pg_rational--0.0.4.sql +++ b/pg_rational--0.0.4.sql @@ -304,6 +304,8 @@ BEGIN ALTER FUNCTION rational_cmp(rational, rational) PARALLEL SAFE; ALTER FUNCTION rational_smaller(rational, rational) PARALLEL SAFE; ALTER FUNCTION rational_larger(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_intermediate(rational, rational) PARALLEL SAFE; + ALTER FUNCTION rational_intermediate(rational, rational, integer) PARALLEL SAFE; $alter$; EXECUTE $agg$ diff --git a/pg_rational.c b/pg_rational.c index 8a5351f..0f48f7a 100644 --- a/pg_rational.c +++ b/pg_rational.c @@ -125,7 +125,13 @@ rational_in_float(PG_FUNCTION_ARGS) sign; Rational *result = palloc(sizeof(Rational)); - if (target == (int32) target) + if (!isfinite(target) || + target < (float8) INT32_MIN || target > (float8) INT32_MAX) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("value too large for rational"))); + + if (target == trunc(target)) { result->numer = (int32) target; result->denom = 1; @@ -134,13 +140,6 @@ rational_in_float(PG_FUNCTION_ARGS) sign = target < 0.0 ? -1 : 1; target = fabs(target); - - if (!(target <= INT32_MAX)) /* also excludes NaNs */ - { - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("value too large for rational"))); - } z = target; prev_denom = 0; result->numer = (int32) round(target); @@ -545,16 +544,18 @@ pg_add_s32_overflow(int32 a, int32 b, int32 *result) int32 gcd(int32 a, int32 b) { - int32 temp; + int64 aa = a, + bb = b, + temp; - while (b != 0) + while (bb != 0) { - temp = a % b; - a = b; - b = temp; + temp = aa % bb; + aa = bb; + bb = temp; } - return a; + return (int32) aa; } bool diff --git a/sql/pg_rational_test.sql b/sql/pg_rational_test.sql index 23b7330..0ea2529 100644 --- a/sql/pg_rational_test.sql +++ b/sql/pg_rational_test.sql @@ -31,6 +31,8 @@ select 1.0000000001::float::rational; select 2147483647::float::rational; select 2147483647.1::float::rational; select 'NAN'::float::rational; +select 'Infinity'::float::rational; +select '-Infinity'::float::rational; -- to float select '1/2'::rational::float; @@ -65,6 +67,10 @@ select rational_simplify('-3/12'); select rational_simplify('-2147483648/2147483647'); -- don't move negative if it would overflow select rational_simplify('1/-2147483648'); +-- minimum numerator with a negative unit denominator remains representable +select rational_simplify(rational_create(-2147483648, -1)); +-- zero with the minimum denominator has the same hash as zero over one +select rational_hash(rational_create(0, -2147483648)) = rational_hash('0/1'::rational); -- biggest value reduces select rational_simplify('2147483647/2147483647'); -- smallest value reduces @@ -289,3 +295,10 @@ select rational_intermediate( '1134903170/701408733', 100 ); + +-- bounded intermediate search is parallel safe + +select proparallel + from pg_proc + where proname = 'rational_intermediate' + and pronargs = 3;