From ece77bd4d6e84830acf28644525a2cdfc8e985a1 Mon Sep 17 00:00:00 2001 From: Artur Zakirov Date: Wed, 7 Feb 2024 16:21:41 +0100 Subject: [PATCH] Add tests and Github workflow (#1) - Add comparison and arithmetic operators - Add tests and Github workflow - Improve the README --- .github/workflows/main.yml | 21 ++ .gitignore | 2 + Makefile | 6 +- README.md | 85 ++++++ saturated_int--0.0.1.sql | 261 ++++++++++++++++-- src/saturated_int.c | 106 ++++++- test/expected/001_saturated_int.out | 221 +++++++++++++++ test/expected/002_saturated_int_agg.out | 21 ++ test/expected/003_saturated_int_binary.out | 23 ++ test/expected/004_saturated_int_parallel.out | 61 ++++ .../expected/004_saturated_int_parallel_1.out | 60 ++++ test/expected/005_saturated_int_index.out | 43 +++ test/sql/001_saturated_int.sql | 60 ++++ test/sql/002_saturated_int_agg.sql | 9 + test/sql/003_saturated_int_binary.sql | 10 + test/sql/004_saturated_int_parallel.sql | 18 ++ test/sql/005_saturated_int_index.sql | 23 ++ 17 files changed, 1002 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 test/expected/001_saturated_int.out create mode 100644 test/expected/002_saturated_int_agg.out create mode 100644 test/expected/003_saturated_int_binary.out create mode 100644 test/expected/004_saturated_int_parallel.out create mode 100644 test/expected/004_saturated_int_parallel_1.out create mode 100644 test/expected/005_saturated_int_index.out create mode 100644 test/sql/001_saturated_int.sql create mode 100644 test/sql/002_saturated_int_agg.sql create mode 100644 test/sql/003_saturated_int_binary.sql create mode 100644 test/sql/004_saturated_int_parallel.sql create mode 100644 test/sql/005_saturated_int_index.sql diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..bfa249b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,21 @@ +name: CI + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + strategy: + matrix: + pg: [16, 15, 14, 13, 12, 11, 10] + name: 🐘 PostgreSQL ${{ matrix.pg }} + runs-on: ubuntu-latest + container: pgxn/pgxn-tools + steps: + - run: pg-start ${{ matrix.pg }} + - uses: actions/checkout@v2 + - run: pg-build-test diff --git a/.gitignore b/.gitignore index 4b05098..0381d27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.o *.so +.deps/ + test/results test/regression.diffs test/regression.out diff --git a/Makefile b/Makefile index d0b51fe..6d70872 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,15 @@ EXTENSION = saturated_int MODULE_big = saturated_int -PG_CONFIG ?= PG_CONFIG +PG_CONFIG ?= pg_config DATA = $(wildcard *--*.sql) OBJS = $(patsubst %.c,%.o,$(wildcard src/*.c)) PGXS := $(shell $(PG_CONFIG) --pgxs) +TESTS = $(sort $(wildcard test/sql/*.sql)) +REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) +REGRESS_OPTS = --inputdir=test --outputdir=test + include $(PGXS) diff --git a/README.md b/README.md index 6a52a41..5f7419f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,88 @@ # pg-saturated_int An integer type with [saturation arithmetic](https://en.wikipedia.org/wiki/Saturation_arithmetic). +The difference from builtin integer type is that if an input value is out of allowed range +then the error `integer out of range` won't be raised. + +## Types + +### `saturated_int` + +`saturated_int` is an integer type which implements [saturation arithmetic](https://en.wikipedia.org/wiki/Saturation_arithmetic). +Allowed range is `-2147483648` to `+2147483647`. + +```sql +-- Cast to saturated_int + +select 999999999999999::saturated_int; + saturated_int +--------------- + 2147483647 + +select 2147483648::saturated_int; + saturated_int +--------------- + 2147483647 + +select (-2147483649)::saturated_int; + saturated_int +--------------- + -2147483648 +``` + +## Supported operators + +`saturated_int` supports comparison (`<`, `<=`, `<>`, `=`, `>`, `>=`) and arithmetic operators. Here is some examples: + +```sql +select 999999999999999::saturated_int > 2147483648::saturated_int; + ?column? +---------- + f + +select 999999999999999::saturated_int = 2147483648::saturated_int; + ?column? +---------- + t + +select 999999999999999::saturated_int * 2147483648::saturated_int; + ?column? +------------ + 2147483647 + +select (-999999999999999)::saturated_int * 2147483648::saturated_int; + ?column? +------------- + -2147483648 + +select 2147483648::saturated_int / (-1)::saturated_int; + ?column? +------------- + -2147483647 +``` + +Note that it is necessary to explicitly cast both of operands to `saturated_int`. Implicit cast isn't supported: + +```sql +select 2147483647::int = 2147483648::saturated_int; +ERROR: operator does not exist: integer = saturated_int at character 24 + +select 2147483647::int * 2147483648::saturated_int; +ERROR: operator does not exist: integer * saturated_int at character 24 +``` + +## Index support + +The extension supports `btree` and `hash` indexes. + +## Installation from source codes + +To install `saturated_int`, execute this in the extension's directory: + +```shell +make install +``` + +> **Notice:** Don't forget to set the `PG_CONFIG` variable (`make PG_CONFIG=...`) +> in case you want to test `saturated_int` on a non-default or custom build of PostgreSQL. +> Read more [here](https://wiki.postgresql.org/wiki/Building_and_Installing_PostgreSQL_Extension_Modules). diff --git a/saturated_int--0.0.1.sql b/saturated_int--0.0.1.sql index 422eb75..31ca53a 100644 --- a/saturated_int--0.0.1.sql +++ b/saturated_int--0.0.1.sql @@ -1,46 +1,275 @@ -CREATE FUNCTION sat_int4_in(cstring) +-- Define saturated_int + +CREATE FUNCTION saturated_int_in(cstring) RETURNS saturated_int AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -CREATE FUNCTION sat_int4_out(saturated_int) +CREATE FUNCTION saturated_int_out(saturated_int) RETURNS cstring AS 'int4out' LANGUAGE internal IMMUTABLE STRICT PARALLEL SAFE; +CREATE FUNCTION saturated_int_recv(internal) +RETURNS saturated_int +AS 'int4recv' +LANGUAGE internal IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION saturated_int_send(saturated_int) +RETURNS bytea +AS 'int4send' +LANGUAGE internal IMMUTABLE PARALLEL SAFE; + CREATE TYPE saturated_int ( - input = sat_int4_in, - output = sat_int4_out, - -- receive = ..., - -- send = ..., + input = saturated_int_in, + output = saturated_int_out, + receive = saturated_int_recv, + send = saturated_int_send, like = integer, category = 'N' ); -CREATE FUNCTION sat_int8to4(bigint) +-- Define casts into saturated_int + +CREATE FUNCTION saturated_int8to4(bigint) RETURNS saturated_int AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE CAST (bigint AS saturated_int) -WITH FUNCTION sat_int8to4(bigint) AS ASSIGNMENT; +WITH FUNCTION saturated_int8to4(bigint) AS ASSIGNMENT; CREATE CAST (integer AS saturated_int) WITHOUT FUNCTION AS ASSIGNMENT; -CREATE FUNCTION sat_int4_sum(state saturated_int, val saturated_int) +CREATE CAST (saturated_int as integer) +WITHOUT FUNCTION AS ASSIGNMENT; + +-- Define comparison operators + +CREATE FUNCTION saturated_int_eq(saturated_int, saturated_int) +RETURNS boolean +AS 'int4eq' +LANGUAGE internal IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION saturated_int_ne(saturated_int, saturated_int) +RETURNS boolean +AS 'int4ne' +LANGUAGE internal IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION saturated_int_lt(saturated_int, saturated_int) +RETURNS boolean +AS 'int4lt' +LANGUAGE internal IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION saturated_int_le(saturated_int, saturated_int) +RETURNS boolean +AS 'int4le' +LANGUAGE internal IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION saturated_int_gt(saturated_int, saturated_int) +RETURNS boolean +AS 'int4gt' +LANGUAGE internal IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION saturated_int_ge(saturated_int, saturated_int) +RETURNS boolean +AS 'int4ge' +LANGUAGE internal IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION saturated_int_cmp(saturated_int, saturated_int) +RETURNS integer +AS 'btint4cmp' +LANGUAGE internal IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION hash_saturated_int(saturated_int) +RETURNS integer +AS 'hashint4' +LANGUAGE internal IMMUTABLE PARALLEL SAFE; + +CREATE OPERATOR = ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_eq, + commutator = '=', + negator = '<>', + restrict = eqsel, + join = eqjoinsel, + hashes, merges +); +COMMENT ON OPERATOR =(saturated_int, saturated_int) IS 'equals'; + +CREATE OPERATOR <> ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_ne, + commutator = '<>', + negator = '=', + restrict = neqsel, + join = neqjoinsel +); +COMMENT ON OPERATOR <>(saturated_int, saturated_int) IS 'not equals'; + +CREATE OPERATOR < ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_lt, + commutator = '>', + negator = '>=', + restrict = scalarltsel, + join = scalarltjoinsel +); +COMMENT ON OPERATOR <(saturated_int, saturated_int) IS 'less than'; + +CREATE OPERATOR > ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_gt, + commutator = '<', + negator = '<=', + restrict = scalargtsel, + join = scalargtjoinsel +); +COMMENT ON OPERATOR >(saturated_int, saturated_int) IS 'greater than'; + +DO $$ +BEGIN + IF current_setting('server_version_num')::int >= 110000 THEN + CREATE OPERATOR <= ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_le, + commutator = '>=', + negator = '>', + restrict = scalarlesel, + join = scalarlejoinsel + ); + + CREATE OPERATOR >= ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_ge, + commutator = '<=', + negator = '<', + restrict = scalargesel, + join = scalargejoinsel + ); + ELSE + CREATE OPERATOR <= ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_le, + commutator = '>=', + negator = '>', + restrict = scalarltsel, + join = scalarltjoinsel + ); + + CREATE OPERATOR >= ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_ge, + commutator = '<=', + negator = '<', + restrict = scalargtsel, + join = scalargtjoinsel + ); + END IF; +END; +$$; +COMMENT ON OPERATOR >=(saturated_int, saturated_int) IS 'greater than or equal'; +COMMENT ON OPERATOR <=(saturated_int, saturated_int) IS 'less than or equal'; + +CREATE OPERATOR CLASS btree_saturated_int_ops +DEFAULT FOR TYPE saturated_int USING btree +AS + OPERATOR 1 <, + OPERATOR 2 <=, + OPERATOR 3 =, + OPERATOR 4 >=, + OPERATOR 5 >, + FUNCTION 1 saturated_int_cmp(saturated_int, saturated_int); + +CREATE OPERATOR CLASS hash_saturated_int_ops +DEFAULT FOR TYPE saturated_int USING hash +AS + OPERATOR 1 =, + FUNCTION 1 hash_saturated_int(saturated_int); + +-- Define arithmetic operators + +CREATE FUNCTION saturated_int_mul(saturated_int, saturated_int) +RETURNS saturated_int +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE OPERATOR * ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_mul, + commutator = '*' +); +COMMENT ON OPERATOR *(saturated_int, saturated_int) IS 'multiply'; + +CREATE FUNCTION saturated_int_div(saturated_int, saturated_int) +RETURNS saturated_int +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE OPERATOR / ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_div +); +COMMENT ON OPERATOR /(saturated_int, saturated_int) IS 'divide'; + +CREATE FUNCTION saturated_int_mod(saturated_int, saturated_int) +RETURNS saturated_int +AS 'int4mod' +LANGUAGE internal IMMUTABLE PARALLEL SAFE; + +CREATE OPERATOR % ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_mod +); +COMMENT ON OPERATOR %(saturated_int, saturated_int) IS 'modulus'; + +CREATE FUNCTION saturated_int_pl(saturated_int, saturated_int) +RETURNS saturated_int +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE OPERATOR + ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_pl, + commutator = '+' +); +COMMENT ON OPERATOR +(saturated_int, saturated_int) IS 'add'; + +CREATE FUNCTION saturated_int_mi(saturated_int, saturated_int) +RETURNS saturated_int +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE OPERATOR - ( + leftarg = saturated_int, + rightarg = saturated_int, + procedure = saturated_int_mi +); +COMMENT ON OPERATOR -(saturated_int, saturated_int) IS 'subtract'; + +-- Define aggregate functions + +CREATE FUNCTION saturated_int_sum(state saturated_int, val saturated_int) RETURNS saturated_int AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE; CREATE AGGREGATE sum(saturated_int) ( - sfunc = sat_int4_sum, + sfunc = saturated_int_sum, stype = saturated_int, - -- combinefunc = ..., - -- msfunc = ..., - -- minvfunc = ..., - -- mfinalfunc = ..., - -- mstype = ..., - -- minitcond = '{0, 0}', + combinefunc = saturated_int_pl, parallel = SAFE ); diff --git a/src/saturated_int.c b/src/saturated_int.c index 13a5357..ef6426e 100644 --- a/src/saturated_int.c +++ b/src/saturated_int.c @@ -1,15 +1,25 @@ +#include "c.h" #include "postgres.h" #include "fmgr.h" #include #include "utils/builtins.h" +#if PG_VERSION_NUM < 150000 +#include "utils/int8.h" +#endif PG_MODULE_MAGIC; -PG_FUNCTION_INFO_V1(sat_int4_in); -PG_FUNCTION_INFO_V1(sat_int8to4); -PG_FUNCTION_INFO_V1(sat_int4_sum); +PG_FUNCTION_INFO_V1(saturated_int_in); +PG_FUNCTION_INFO_V1(saturated_int8to4); + +PG_FUNCTION_INFO_V1(saturated_int_sum); + +PG_FUNCTION_INFO_V1(saturated_int_mul); +PG_FUNCTION_INFO_V1(saturated_int_div); +PG_FUNCTION_INFO_V1(saturated_int_pl); +PG_FUNCTION_INFO_V1(saturated_int_mi); /* * Cast bigint to integer with saturation. @@ -18,7 +28,7 @@ PG_FUNCTION_INFO_V1(sat_int4_sum); * Return INT_MAX if the parsed value greater than INT_MAX. */ static inline int32 -sat_int8to4_impl(int64 num) +saturated_int8to4_impl(int64 num) { if (unlikely(num < INT_MIN)) return INT_MIN; @@ -32,29 +42,35 @@ sat_int8to4_impl(int64 num) * Parse integer with saturation. */ Datum -sat_int4_in(PG_FUNCTION_ARGS) +saturated_int_in(PG_FUNCTION_ARGS) { char *arg = PG_GETARG_CSTRING(0); - PG_RETURN_INT32(sat_int8to4_impl(pg_strtoint64(arg))); +#if PG_VERSION_NUM < 150000 + int64 result; + (void) scanint8(arg, false, &result); + PG_RETURN_INT32(saturated_int8to4_impl(result)); +#else + PG_RETURN_INT32(saturated_int8to4_impl(pg_strtoint64(arg))); +#endif } /* * Cast bigint to integer with saturation. */ Datum -sat_int8to4(PG_FUNCTION_ARGS) +saturated_int8to4(PG_FUNCTION_ARGS) { int64 arg = PG_GETARG_INT64(0); - PG_RETURN_INT32(sat_int8to4_impl(arg)); + PG_RETURN_INT32(saturated_int8to4_impl(arg)); } /* * Compute the saturating addition (https://en.wikipedia.org/wiki/Saturation_arithmetic). */ Datum -sat_int4_sum(PG_FUNCTION_ARGS) +saturated_int_sum(PG_FUNCTION_ARGS) { int32 oldsum = PG_GETARG_INT32(0); int32 newval; @@ -78,11 +94,79 @@ sat_int4_sum(PG_FUNCTION_ARGS) * If oldsum < 0 there can be only overflow if newval < INT_MIN - oldsum. */ newval = PG_GETARG_INT32(1); - if (oldsum >= 0 && newval > INT_MAX - oldsum) + if (unlikely(oldsum >= 0 && newval > INT_MAX - oldsum)) PG_RETURN_INT32(INT_MAX); - else if (oldsum < 0 && newval < INT_MIN - oldsum) + else if (unlikely(oldsum < 0 && newval < INT_MIN - oldsum)) PG_RETURN_INT32(INT_MIN); /* It is safe to return sum */ PG_RETURN_INT32(oldsum + newval); } + +/* + * Arithmetic functions for operators. + */ + +Datum +saturated_int_mul(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_INT32(saturated_int8to4_impl((int64) arg1 * (int64) arg2)); +} + +/* + * Copy of int4div() with changes for saturated_int. + */ +Datum +saturated_int_div(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + int32 result; + + if (unlikely(arg2 == 0)) + { + ereport(ERROR, + (errcode(ERRCODE_DIVISION_BY_ZERO), + errmsg("division by zero"))); + /* ensure compiler realizes we mustn't reach the division (gcc bug) */ + PG_RETURN_NULL(); + } + + /* + * INT_MIN / -1 will return INT_MAX, which isn't exactly just negation. + */ + if (arg2 == -1) + { + if (unlikely(arg1 == PG_INT32_MIN)) + PG_RETURN_INT32(PG_INT32_MAX); + result = -arg1; + PG_RETURN_INT32(result); + } + + /* No overflow is possible */ + + result = arg1 / arg2; + + PG_RETURN_INT32(result); +} + +Datum +saturated_int_pl(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_INT32(saturated_int8to4_impl((int64) arg1 + (int64) arg2)); +} + +Datum +saturated_int_mi(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_INT32(saturated_int8to4_impl((int64) arg1 - (int64) arg2)); +} diff --git a/test/expected/001_saturated_int.out b/test/expected/001_saturated_int.out new file mode 100644 index 0000000..d09f5bf --- /dev/null +++ b/test/expected/001_saturated_int.out @@ -0,0 +1,221 @@ +-- Show less information in error messages due to different output of different +-- PostgreSQL versions +\set VERBOSITY terse +create extension saturated_int; +-- Test cast to saturated_int +select 999999999999999::saturated_int; + saturated_int +--------------- + 2147483647 +(1 row) + +select 2147483648::saturated_int; + saturated_int +--------------- + 2147483647 +(1 row) + +select 2147483647::saturated_int; + saturated_int +--------------- + 2147483647 +(1 row) + +select (-2147483649)::saturated_int; + saturated_int +--------------- + -2147483648 +(1 row) + +select (-2147483648)::saturated_int; + saturated_int +--------------- + -2147483648 +(1 row) + +-- Test comparison operators +select 999999999999999::saturated_int < 2147483648::saturated_int; + ?column? +---------- + f +(1 row) + +select 999999999999999::saturated_int > 2147483648::saturated_int; + ?column? +---------- + f +(1 row) + +select 999999999999999::saturated_int = 2147483648::saturated_int; + ?column? +---------- + t +(1 row) + +select 2147483647::saturated_int = 2147483648::saturated_int; + ?column? +---------- + t +(1 row) + +select 2147483647::saturated_int <= 2147483648::saturated_int; + ?column? +---------- + t +(1 row) + +select 2147483646::saturated_int = 2147483648::saturated_int; + ?column? +---------- + f +(1 row) + +select 2147483646::saturated_int < 2147483648::saturated_int; + ?column? +---------- + t +(1 row) + +select 2147483646::saturated_int <= 2147483648::saturated_int; + ?column? +---------- + t +(1 row) + +select 2147483647::saturated_int > 2147483648::saturated_int; + ?column? +---------- + f +(1 row) + +select 2147483647::saturated_int >= 2147483648::saturated_int; + ?column? +---------- + t +(1 row) + +-- ERROR: operator does not exist +select 2147483647::int = 2147483648::saturated_int; +ERROR: operator does not exist: integer = saturated_int at character 24 +select 2147483647::int > 2147483648::saturated_int; +ERROR: operator does not exist: integer > saturated_int at character 24 +select 2147483647::int >= 2147483648::saturated_int; +ERROR: operator does not exist: integer >= saturated_int at character 24 +select 2147483647::int <= 2147483648::saturated_int; +ERROR: operator does not exist: integer <= saturated_int at character 24 +select 2147483647::int < 2147483648::saturated_int; +ERROR: operator does not exist: integer < saturated_int at character 24 +-- Test arithmetic operators +select 999999999999999::saturated_int * 2147483648::saturated_int; + ?column? +------------ + 2147483647 +(1 row) + +select 9::saturated_int * 9::saturated_int; + ?column? +---------- + 81 +(1 row) + +select (-999999999999999)::saturated_int * 2147483648::saturated_int; + ?column? +------------- + -2147483648 +(1 row) + +select 999999999999999::saturated_int / 2147483648::saturated_int; + ?column? +---------- + 1 +(1 row) + +select 9::saturated_int / 3::saturated_int; + ?column? +---------- + 3 +(1 row) + +select (-2147483648)::saturated_int / 2::saturated_int; + ?column? +------------- + -1073741824 +(1 row) + +select 2147483648::saturated_int / (-1)::saturated_int; + ?column? +------------- + -2147483647 +(1 row) + +select 999999999999999::saturated_int % 2147483648::saturated_int; + ?column? +---------- + 0 +(1 row) + +select 9::saturated_int % 4::saturated_int; + ?column? +---------- + 1 +(1 row) + +select (-999999999999999)::saturated_int % 2147483648::saturated_int; + ?column? +---------- + -1 +(1 row) + +select 999999999999999::saturated_int + 2147483648::saturated_int; + ?column? +------------ + 2147483647 +(1 row) + +select 9::saturated_int + 9::saturated_int; + ?column? +---------- + 18 +(1 row) + +select (-999999999999999)::saturated_int + 3::saturated_int; + ?column? +------------- + -2147483645 +(1 row) + +select 999999999999999::saturated_int - 2147483648::saturated_int; + ?column? +---------- + 0 +(1 row) + +select 9::saturated_int - 3::saturated_int; + ?column? +---------- + 6 +(1 row) + +select (-999999999999999)::saturated_int - 2147483648::saturated_int; + ?column? +------------- + -2147483648 +(1 row) + +-- ERROR: operator does not exist +select 2147483647::int * 2147483648::saturated_int; +ERROR: operator does not exist: integer * saturated_int at character 24 +select 2147483647::int / 2147483648::saturated_int; +ERROR: operator does not exist: integer / saturated_int at character 24 +select 2147483647::saturated_int / 2147483647::int; +ERROR: operator does not exist: saturated_int / integer at character 34 +select 2147483647::int % 2147483648::saturated_int; +ERROR: operator does not exist: integer % saturated_int at character 24 +select 2147483647::saturated_int % 2147483647::int; +ERROR: operator does not exist: saturated_int % integer at character 34 +select 2147483647::int + 2147483648::saturated_int; +ERROR: operator does not exist: integer + saturated_int at character 24 +select 2147483647::int - 2147483648::saturated_int; +ERROR: operator does not exist: integer - saturated_int at character 24 +select 2147483647::saturated_int - 2147483647::int; +ERROR: operator does not exist: saturated_int - integer at character 34 diff --git a/test/expected/002_saturated_int_agg.out b/test/expected/002_saturated_int_agg.out new file mode 100644 index 0000000..2b2810d --- /dev/null +++ b/test/expected/002_saturated_int_agg.out @@ -0,0 +1,21 @@ +CREATE TABLE sat_agg (id1 saturated_int, id2 saturated_int); +INSERT INTO sat_agg +SELECT i, 1 FROM pg_catalog.generate_series(2147483645::bigint, 2147483650::bigint) g(i); +SELECT * FROM sat_agg; + id1 | id2 +------------+----- + 2147483645 | 1 + 2147483646 | 1 + 2147483647 | 1 + 2147483647 | 1 + 2147483647 | 1 + 2147483647 | 1 +(6 rows) + +-- Test aggregate functions +SELECT sum(id1), sum(id2) FROM sat_agg; + sum | sum +------------+----- + 2147483647 | 6 +(1 row) + diff --git a/test/expected/003_saturated_int_binary.out b/test/expected/003_saturated_int_binary.out new file mode 100644 index 0000000..b731d5c --- /dev/null +++ b/test/expected/003_saturated_int_binary.out @@ -0,0 +1,23 @@ +CREATE TABLE before (i saturated_int); +CREATE TABLE after (i saturated_int); +INSERT INTO before VALUES (1), (-1), (2147483649), (-2147483649); +SELECT * FROM before; + i +------------- + 1 + -1 + 2147483647 + -2147483648 +(4 rows) + +COPY before TO '/tmp/tst' WITH (FORMAT binary); +COPY after FROM '/tmp/tst' WITH (FORMAT binary); +SELECT * FROM after; + i +------------- + 1 + -1 + 2147483647 + -2147483648 +(4 rows) + diff --git a/test/expected/004_saturated_int_parallel.out b/test/expected/004_saturated_int_parallel.out new file mode 100644 index 0000000..fc4a626 --- /dev/null +++ b/test/expected/004_saturated_int_parallel.out @@ -0,0 +1,61 @@ +SET max_parallel_workers_per_gather=4; +SET parallel_setup_cost = 10; +SET parallel_tuple_cost = 0.001; +CREATE TABLE parallel_test(id1 saturated_int, id2 int) WITH (parallel_workers = 4); +INSERT INTO parallel_test +SELECT i, i FROM generate_series(1::int, 1e4::int) g(i); +EXPLAIN (COSTS OFF, VERBOSE) +SELECT count(*) FROM parallel_test WHERE id1 = 5000::saturated_int; + QUERY PLAN +------------------------------------------------------------------------- + Finalize Aggregate + Output: count(*) + -> Gather + Output: (PARTIAL count(*)) + Workers Planned: 4 + -> Partial Aggregate + Output: PARTIAL count(*) + -> Parallel Seq Scan on public.parallel_test + Output: id1, id2 + Filter: (parallel_test.id1 = '5000'::saturated_int) +(10 rows) + +EXPLAIN (COSTS OFF, VERBOSE) +SELECT id1, count(*) FROM parallel_test GROUP BY 1; + QUERY PLAN +------------------------------------------------------------- + Finalize HashAggregate + Output: id1, count(*) + Group Key: parallel_test.id1 + -> Gather + Output: id1, (PARTIAL count(*)) + Workers Planned: 4 + -> Partial HashAggregate + Output: id1, PARTIAL count(*) + Group Key: parallel_test.id1 + -> Parallel Seq Scan on public.parallel_test + Output: id1, id2 +(11 rows) + +EXPLAIN (COSTS OFF, VERBOSE) +SELECT sum(id1) FROM parallel_test WHERE id1 < 5000::saturated_int; + QUERY PLAN +------------------------------------------------------------------------- + Finalize Aggregate + Output: sum(id1) + -> Gather + Output: (PARTIAL sum(id1)) + Workers Planned: 4 + -> Partial Aggregate + Output: PARTIAL sum(id1) + -> Parallel Seq Scan on public.parallel_test + Output: id1, id2 + Filter: (parallel_test.id1 < '5000'::saturated_int) +(10 rows) + +SELECT sum(id1), sum(id2) FROM parallel_test WHERE id1 < 5000::saturated_int; + sum | sum +----------+---------- + 12497500 | 12497500 +(1 row) + diff --git a/test/expected/004_saturated_int_parallel_1.out b/test/expected/004_saturated_int_parallel_1.out new file mode 100644 index 0000000..2543a19 --- /dev/null +++ b/test/expected/004_saturated_int_parallel_1.out @@ -0,0 +1,60 @@ +SET max_parallel_workers_per_gather=4; +SET parallel_setup_cost = 10; +SET parallel_tuple_cost = 0.001; +CREATE TABLE parallel_test(id1 saturated_int, id2 int) WITH (parallel_workers = 4); +INSERT INTO parallel_test +SELECT i, i FROM generate_series(1::int, 1e4::int) g(i); +EXPLAIN (COSTS OFF, VERBOSE) +SELECT count(*) FROM parallel_test WHERE id1 = 5000::saturated_int; + QUERY PLAN +------------------------------------------------------------------------- + Finalize Aggregate + Output: count(*) + -> Gather + Output: (PARTIAL count(*)) + Workers Planned: 4 + -> Partial Aggregate + Output: PARTIAL count(*) + -> Parallel Seq Scan on public.parallel_test + Filter: (parallel_test.id1 = '5000'::saturated_int) +(9 rows) + +EXPLAIN (COSTS OFF, VERBOSE) +SELECT id1, count(*) FROM parallel_test GROUP BY 1; + QUERY PLAN +------------------------------------------------------------- + Finalize HashAggregate + Output: id1, count(*) + Group Key: parallel_test.id1 + -> Gather + Output: id1, (PARTIAL count(*)) + Workers Planned: 4 + -> Partial HashAggregate + Output: id1, PARTIAL count(*) + Group Key: parallel_test.id1 + -> Parallel Seq Scan on public.parallel_test + Output: id1 +(11 rows) + +EXPLAIN (COSTS OFF, VERBOSE) +SELECT sum(id1) FROM parallel_test WHERE id1 < 5000::saturated_int; + QUERY PLAN +------------------------------------------------------------------------- + Finalize Aggregate + Output: sum(id1) + -> Gather + Output: (PARTIAL sum(id1)) + Workers Planned: 4 + -> Partial Aggregate + Output: PARTIAL sum(id1) + -> Parallel Seq Scan on public.parallel_test + Output: id1 + Filter: (parallel_test.id1 < '5000'::saturated_int) +(10 rows) + +SELECT sum(id1), sum(id2) FROM parallel_test WHERE id1 < 5000::saturated_int; + sum | sum +----------+---------- + 12497500 | 12497500 +(1 row) + diff --git a/test/expected/005_saturated_int_index.out b/test/expected/005_saturated_int_index.out new file mode 100644 index 0000000..42996d7 --- /dev/null +++ b/test/expected/005_saturated_int_index.out @@ -0,0 +1,43 @@ +SET enable_seqscan to off; +CREATE TABLE btree_test(id saturated_int); +INSERT INTO btree_test +SELECT i FROM generate_series(1::int, 1e4::int) g(i); +CREATE INDEX btree_test_idx ON btree_test (id); +EXPLAIN (COSTS OFF, VERBOSE) +SELECT id FROM btree_test WHERE id = 5000::saturated_int; + QUERY PLAN +------------------------------------------------------------- + Bitmap Heap Scan on public.btree_test + Output: id + Recheck Cond: (btree_test.id = '5000'::saturated_int) + -> Bitmap Index Scan on btree_test_idx + Index Cond: (btree_test.id = '5000'::saturated_int) +(5 rows) + +SELECT id FROM btree_test WHERE id = 5000::saturated_int; + id +------ + 5000 +(1 row) + +CREATE TABLE hash_test(id saturated_int); +INSERT INTO hash_test +SELECT i FROM generate_series(1::int, 1e4::int) g(i); +CREATE INDEX hash_test_idx ON hash_test (id); +EXPLAIN (COSTS OFF, VERBOSE) +SELECT id FROM hash_test WHERE id = 5000::saturated_int; + QUERY PLAN +------------------------------------------------------------ + Bitmap Heap Scan on public.hash_test + Output: id + Recheck Cond: (hash_test.id = '5000'::saturated_int) + -> Bitmap Index Scan on hash_test_idx + Index Cond: (hash_test.id = '5000'::saturated_int) +(5 rows) + +SELECT id FROM hash_test WHERE id = 5000::saturated_int; + id +------ + 5000 +(1 row) + diff --git a/test/sql/001_saturated_int.sql b/test/sql/001_saturated_int.sql new file mode 100644 index 0000000..1252b02 --- /dev/null +++ b/test/sql/001_saturated_int.sql @@ -0,0 +1,60 @@ +-- Show less information in error messages due to different output of different +-- PostgreSQL versions +\set VERBOSITY terse + +create extension saturated_int; + +-- Test cast to saturated_int + +select 999999999999999::saturated_int; +select 2147483648::saturated_int; +select 2147483647::saturated_int; +select (-2147483649)::saturated_int; +select (-2147483648)::saturated_int; + +-- Test comparison operators + +select 999999999999999::saturated_int < 2147483648::saturated_int; +select 999999999999999::saturated_int > 2147483648::saturated_int; +select 999999999999999::saturated_int = 2147483648::saturated_int; +select 2147483647::saturated_int = 2147483648::saturated_int; +select 2147483647::saturated_int <= 2147483648::saturated_int; +select 2147483646::saturated_int = 2147483648::saturated_int; +select 2147483646::saturated_int < 2147483648::saturated_int; +select 2147483646::saturated_int <= 2147483648::saturated_int; +select 2147483647::saturated_int > 2147483648::saturated_int; +select 2147483647::saturated_int >= 2147483648::saturated_int; +-- ERROR: operator does not exist +select 2147483647::int = 2147483648::saturated_int; +select 2147483647::int > 2147483648::saturated_int; +select 2147483647::int >= 2147483648::saturated_int; +select 2147483647::int <= 2147483648::saturated_int; +select 2147483647::int < 2147483648::saturated_int; + +-- Test arithmetic operators + +select 999999999999999::saturated_int * 2147483648::saturated_int; +select 9::saturated_int * 9::saturated_int; +select (-999999999999999)::saturated_int * 2147483648::saturated_int; +select 999999999999999::saturated_int / 2147483648::saturated_int; +select 9::saturated_int / 3::saturated_int; +select (-2147483648)::saturated_int / 2::saturated_int; +select 2147483648::saturated_int / (-1)::saturated_int; +select 999999999999999::saturated_int % 2147483648::saturated_int; +select 9::saturated_int % 4::saturated_int; +select (-999999999999999)::saturated_int % 2147483648::saturated_int; +select 999999999999999::saturated_int + 2147483648::saturated_int; +select 9::saturated_int + 9::saturated_int; +select (-999999999999999)::saturated_int + 3::saturated_int; +select 999999999999999::saturated_int - 2147483648::saturated_int; +select 9::saturated_int - 3::saturated_int; +select (-999999999999999)::saturated_int - 2147483648::saturated_int; +-- ERROR: operator does not exist +select 2147483647::int * 2147483648::saturated_int; +select 2147483647::int / 2147483648::saturated_int; +select 2147483647::saturated_int / 2147483647::int; +select 2147483647::int % 2147483648::saturated_int; +select 2147483647::saturated_int % 2147483647::int; +select 2147483647::int + 2147483648::saturated_int; +select 2147483647::int - 2147483648::saturated_int; +select 2147483647::saturated_int - 2147483647::int; diff --git a/test/sql/002_saturated_int_agg.sql b/test/sql/002_saturated_int_agg.sql new file mode 100644 index 0000000..e38e424 --- /dev/null +++ b/test/sql/002_saturated_int_agg.sql @@ -0,0 +1,9 @@ +CREATE TABLE sat_agg (id1 saturated_int, id2 saturated_int); + +INSERT INTO sat_agg +SELECT i, 1 FROM pg_catalog.generate_series(2147483645::bigint, 2147483650::bigint) g(i); + +SELECT * FROM sat_agg; + +-- Test aggregate functions +SELECT sum(id1), sum(id2) FROM sat_agg; diff --git a/test/sql/003_saturated_int_binary.sql b/test/sql/003_saturated_int_binary.sql new file mode 100644 index 0000000..a6307cd --- /dev/null +++ b/test/sql/003_saturated_int_binary.sql @@ -0,0 +1,10 @@ +CREATE TABLE before (i saturated_int); +CREATE TABLE after (i saturated_int); + +INSERT INTO before VALUES (1), (-1), (2147483649), (-2147483649); +SELECT * FROM before; + +COPY before TO '/tmp/tst' WITH (FORMAT binary); +COPY after FROM '/tmp/tst' WITH (FORMAT binary); + +SELECT * FROM after; diff --git a/test/sql/004_saturated_int_parallel.sql b/test/sql/004_saturated_int_parallel.sql new file mode 100644 index 0000000..7bdf79d --- /dev/null +++ b/test/sql/004_saturated_int_parallel.sql @@ -0,0 +1,18 @@ +SET max_parallel_workers_per_gather=4; +SET parallel_setup_cost = 10; +SET parallel_tuple_cost = 0.001; + +CREATE TABLE parallel_test(id1 saturated_int, id2 int) WITH (parallel_workers = 4); +INSERT INTO parallel_test +SELECT i, i FROM generate_series(1::int, 1e4::int) g(i); + +EXPLAIN (COSTS OFF, VERBOSE) +SELECT count(*) FROM parallel_test WHERE id1 = 5000::saturated_int; + +EXPLAIN (COSTS OFF, VERBOSE) +SELECT id1, count(*) FROM parallel_test GROUP BY 1; + +EXPLAIN (COSTS OFF, VERBOSE) +SELECT sum(id1) FROM parallel_test WHERE id1 < 5000::saturated_int; + +SELECT sum(id1), sum(id2) FROM parallel_test WHERE id1 < 5000::saturated_int; diff --git a/test/sql/005_saturated_int_index.sql b/test/sql/005_saturated_int_index.sql new file mode 100644 index 0000000..568d949 --- /dev/null +++ b/test/sql/005_saturated_int_index.sql @@ -0,0 +1,23 @@ +SET enable_seqscan to off; + +CREATE TABLE btree_test(id saturated_int); +INSERT INTO btree_test +SELECT i FROM generate_series(1::int, 1e4::int) g(i); + +CREATE INDEX btree_test_idx ON btree_test (id); + +EXPLAIN (COSTS OFF, VERBOSE) +SELECT id FROM btree_test WHERE id = 5000::saturated_int; + +SELECT id FROM btree_test WHERE id = 5000::saturated_int; + +CREATE TABLE hash_test(id saturated_int); +INSERT INTO hash_test +SELECT i FROM generate_series(1::int, 1e4::int) g(i); + +CREATE INDEX hash_test_idx ON hash_test (id); + +EXPLAIN (COSTS OFF, VERBOSE) +SELECT id FROM hash_test WHERE id = 5000::saturated_int; + +SELECT id FROM hash_test WHERE id = 5000::saturated_int;