From 11d109d7a8fb0f8e151596309b3dd2c8d9827cd8 Mon Sep 17 00:00:00 2001 From: Ibrar Ahmed Date: Sat, 16 May 2026 06:52:54 +0500 Subject: [PATCH] SUP-140: fix MAXVALUE on snowflake-converted sequences convert_sequence_to_snowflake() previously set MAXVALUE to (old last_value + 1). snowflake.nextval() bypasses MAXVALUE, so the sequence stored huge snowflake IDs against a tiny ceiling; pg_dump then captured both, and the restore failed with "setval: value is out of bounds for sequence ...". Fix in every embedded copy: ALTER SEQUENCE ... AS bigint NO CYCLE MAXVALUE 9223372036854775807. AS bigint keeps the fix safe for int4-backed serial sequences. Adds snowflake--2.4--2.5.sql which re-installs the corrected function and repairs already-converted sequences in existing databases. Bumps default_version to 2.5. Regression test sql/maxvalue.sql exercises the pg_dump setval path. --- Makefile | 5 +- expected/conversion.out | 18 +-- expected/maxvalue.out | 100 +++++++++++++ snowflake--1.2--2.0.sql | 21 +-- snowflake--2.0--2.2.sql | 21 +-- snowflake--2.0.sql | 21 +-- snowflake--2.2--2.3.sql | 21 +-- snowflake--2.2.sql | 21 +-- snowflake--2.3--2.4.sql | 21 +-- snowflake--2.3.sql | 21 +-- snowflake--2.4--2.5.sql | 313 ++++++++++++++++++++++++++++++++++++++++ snowflake.control | 2 +- sql/maxvalue.sql | 68 +++++++++ 13 files changed, 536 insertions(+), 117 deletions(-) create mode 100644 expected/maxvalue.out create mode 100644 snowflake--2.4--2.5.sql create mode 100644 sql/maxvalue.sql diff --git a/Makefile b/Makefile index 75224af..c5d6437 100644 --- a/Makefile +++ b/Makefile @@ -17,10 +17,11 @@ DATA = snowflake--1.0.sql \ snowflake--2.2.sql \ snowflake--2.2--2.3.sql \ snowflake--2.3.sql \ - snowflake--2.3--2.4.sql + snowflake--2.3--2.4.sql \ + snowflake--2.4--2.5.sql PGFILEDESC = "snowflake - snowflake style IDs for PostgreSQL" -REGRESS = conversion +REGRESS = conversion maxvalue ifdef USE_PGXS PG_CONFIG = pg_config diff --git a/expected/conversion.out b/expected/conversion.out index 7945913..3a7d117 100644 --- a/expected/conversion.out +++ b/expected/conversion.out @@ -17,7 +17,7 @@ INSERT INTO t3 VALUES (DEFAULT); SELECT snowflake.convert_sequence_to_snowflake('t1'); -- ERROR, not a sequence ERROR: Input value "public.t1" is not a valid convertable sequence SELECT snowflake.convert_sequence_to_snowflake('seq_1'); -- No associated relation found -NOTICE: ALTER SEQUENCE public.seq_1 NO CYCLE MAXVALUE 43 +NOTICE: ALTER SEQUENCE public.seq_1 AS bigint NO CYCLE MAXVALUE 9223372036854775807 convert_sequence_to_snowflake ------------------------------- 0 @@ -26,7 +26,7 @@ NOTICE: ALTER SEQUENCE public.seq_1 NO CYCLE MAXVALUE 43 SELECT snowflake.convert_sequence_to_snowflake('t1_x_seq'); NOTICE: EXECUTE ALTER TABLE public.t1 ALTER COLUMN x SET DATA TYPE int8 NOTICE: EXECUTE ALTER TABLE public.t1 ALTER COLUMN x SET DEFAULT snowflake.nextval('public.t1_x_seq'::regclass) -NOTICE: ALTER SEQUENCE public.t1_x_seq NO CYCLE MAXVALUE 2 +NOTICE: ALTER SEQUENCE public.t1_x_seq AS bigint NO CYCLE MAXVALUE 9223372036854775807 convert_sequence_to_snowflake ------------------------------- 1 @@ -36,7 +36,7 @@ SELECT snowflake.convert_sequence_to_snowflake('t2_x_seq'); NOTICE: Update pg_attribute: reset attidentity value for table public.t2, column x NOTICE: EXECUTE ALTER TABLE public.t2 ALTER COLUMN x SET DATA TYPE int8 NOTICE: EXECUTE ALTER TABLE public.t2 ALTER COLUMN x SET DEFAULT snowflake.nextval('public.t2_x_seq'::regclass) -NOTICE: ALTER SEQUENCE public.t2_x_seq NO CYCLE MAXVALUE 2 +NOTICE: ALTER SEQUENCE public.t2_x_seq AS bigint NO CYCLE MAXVALUE 9223372036854775807 convert_sequence_to_snowflake ------------------------------- 1 @@ -46,7 +46,7 @@ SELECT snowflake.convert_sequence_to_snowflake('t3_x_seq'); NOTICE: Update pg_attribute: reset attidentity value for table public.t3, column x NOTICE: EXECUTE ALTER TABLE public.t3 ALTER COLUMN x SET DATA TYPE int8 NOTICE: EXECUTE ALTER TABLE public.t3 ALTER COLUMN x SET DEFAULT snowflake.nextval('public.t3_x_seq'::regclass) -NOTICE: ALTER SEQUENCE public.t3_x_seq NO CYCLE MAXVALUE 2 +NOTICE: ALTER SEQUENCE public.t3_x_seq AS bigint NO CYCLE MAXVALUE 9223372036854775807 convert_sequence_to_snowflake ------------------------------- 1 @@ -55,7 +55,7 @@ NOTICE: ALTER SEQUENCE public.t3_x_seq NO CYCLE MAXVALUE 2 SELECT snowflake.convert_sequence_to_snowflake('t3_z_seq'); -- non-default attnum NOTICE: EXECUTE ALTER TABLE public.t3 ALTER COLUMN z SET DATA TYPE int8 NOTICE: EXECUTE ALTER TABLE public.t3 ALTER COLUMN z SET DEFAULT snowflake.nextval('public.t3_z_seq'::regclass) -NOTICE: ALTER SEQUENCE public.t3_z_seq NO CYCLE MAXVALUE 2 +NOTICE: ALTER SEQUENCE public.t3_z_seq AS bigint NO CYCLE MAXVALUE 9223372036854775807 convert_sequence_to_snowflake ------------------------------- 1 @@ -114,7 +114,7 @@ ALTER TABLE t3 ALTER COLUMN y SET DEFAULT nextval('t3_y_seq'::regclass); SELECT snowflake.convert_sequence_to_snowflake('t3_y_seq'); NOTICE: EXECUTE ALTER TABLE public.t3 ALTER COLUMN y SET DATA TYPE int8 NOTICE: EXECUTE ALTER TABLE public.t3 ALTER COLUMN y SET DEFAULT snowflake.nextval('public.t3_y_seq'::regclass) -NOTICE: ALTER SEQUENCE public.t3_y_seq NO CYCLE MAXVALUE 3 +NOTICE: ALTER SEQUENCE public.t3_y_seq AS bigint NO CYCLE MAXVALUE 9223372036854775807 convert_sequence_to_snowflake ------------------------------- 1 @@ -162,7 +162,7 @@ SELECT * FROM t4; SELECT snowflake.convert_sequence_to_snowflake('favorite_seq'); NOTICE: EXECUTE ALTER TABLE public.t4 ALTER COLUMN x SET DATA TYPE int8 NOTICE: EXECUTE ALTER TABLE public.t4 ALTER COLUMN x SET DEFAULT snowflake.nextval('public.favorite_seq'::regclass) -NOTICE: ALTER SEQUENCE public.favorite_seq NO CYCLE MAXVALUE 2 +NOTICE: ALTER SEQUENCE public.favorite_seq AS bigint NO CYCLE MAXVALUE 9223372036854775807 convert_sequence_to_snowflake ------------------------------- 2 @@ -201,7 +201,7 @@ NOTICE: EXECUTE ALTER TABLE public.t4 ALTER COLUMN x SET DATA TYPE int8 NOTICE: EXECUTE ALTER TABLE public.t4 ALTER COLUMN x SET DEFAULT snowflake.nextval('public.favorite_seq'::regclass) NOTICE: EXECUTE ALTER TABLE public.t5 ALTER COLUMN x SET DATA TYPE int8 NOTICE: EXECUTE ALTER TABLE public.t5 ALTER COLUMN x SET DEFAULT snowflake.nextval('public.favorite_seq'::regclass) -NOTICE: ALTER SEQUENCE public.favorite_seq NO CYCLE MAXVALUE 3 +NOTICE: ALTER SEQUENCE public.favorite_seq AS bigint NO CYCLE MAXVALUE 9223372036854775807 convert_sequence_to_snowflake ------------------------------- 4 @@ -238,7 +238,7 @@ SELECT * FROM t6; SELECT snowflake.convert_sequence_to_snowflake('t6_x_seq'); NOTICE: EXECUTE ALTER TABLE public.t6 ALTER COLUMN x SET DEFAULT snowflake.nextval('public.t6_x_seq'::regclass) -NOTICE: ALTER SEQUENCE public.t6_x_seq NO CYCLE MAXVALUE 4 +NOTICE: ALTER SEQUENCE public.t6_x_seq AS bigint NO CYCLE MAXVALUE 9223372036854775807 convert_sequence_to_snowflake ------------------------------- 0 diff --git a/expected/maxvalue.out b/expected/maxvalue.out new file mode 100644 index 0000000..2ce4644 --- /dev/null +++ b/expected/maxvalue.out @@ -0,0 +1,100 @@ +/* + * maxvalue.sql - regression for the SUP-140 dump/restore failure. + * + * Before the fix, convert_sequence_to_snowflake() set + * ALTER SEQUENCE ... MAXVALUE (last_value + 1) + * which is fine for the snowflake.nextval() path (it bypasses MAXVALUE) + * but fatal for pg_dump/restore: the dump captures the resulting + * snowflake-sized last_value via pg_catalog.setval(...) and the + * restore is rejected with + * ERROR: setval: value is out of bounds for + * sequence "" (1..) + * + * This test asserts that after conversion the sequence's max_value is + * the full bigint range (2^63-1). + */ +\set VERBOSITY terse +SET snowflake.node = 1; +CREATE EXTENSION snowflake; +-- ---------------------------------------------------------------------- +-- 1. Customer reproducer: bigserial table, INSERT, convert. +-- ---------------------------------------------------------------------- +CREATE TABLE orders ( + id bigserial PRIMARY KEY, + customer text NOT NULL, + amount numeric NOT NULL +); +INSERT INTO orders (customer, amount) VALUES + ('Alice', 100.00), + ('Bob', 250.00), + ('Carol', 75.50); +SELECT snowflake.convert_sequence_to_snowflake('orders_id_seq'::regclass); +NOTICE: EXECUTE ALTER TABLE public.orders ALTER COLUMN id SET DEFAULT snowflake.nextval('public.orders_id_seq'::regclass) +NOTICE: ALTER SEQUENCE public.orders_id_seq AS bigint NO CYCLE MAXVALUE 9223372036854775807 + convert_sequence_to_snowflake +------------------------------- + 0 +(1 row) + +-- After conversion max_value MUST be the bigint ceiling, not (last_value+1). +SELECT max_value = 9223372036854775807 AS max_is_bigint_max +FROM pg_sequences WHERE sequencename = 'orders_id_seq'; + max_is_bigint_max +------------------- + t +(1 row) + +-- ---------------------------------------------------------------------- +-- 2. Generate a real snowflake-sized id; setval to that value must succeed +-- (this is what pg_dump emits in the restore script). +-- ---------------------------------------------------------------------- +INSERT INTO orders (customer, amount) VALUES ('Dave', 999) RETURNING id > 9999; + ?column? +---------- + t +(1 row) + +-- A pg_catalog.setval() with a snowflake-sized value must NOT raise +-- "out of bounds for sequence" any more. +SELECT pg_catalog.setval('orders_id_seq', 4446196691613229056, true); + setval +--------------------- + 4446196691613229056 +(1 row) + +SELECT last_value FROM orders_id_seq; + last_value +--------------------- + 4446196691613229056 +(1 row) + +-- ---------------------------------------------------------------------- +-- 3. Plain (un-owned) sequence path. +-- ---------------------------------------------------------------------- +CREATE SEQUENCE standalone_seq START 100; +SELECT snowflake.convert_sequence_to_snowflake('standalone_seq'::regclass); +NOTICE: ALTER SEQUENCE public.standalone_seq AS bigint NO CYCLE MAXVALUE 9223372036854775807 + convert_sequence_to_snowflake +------------------------------- + 0 +(1 row) + +SELECT max_value = 9223372036854775807 AS max_is_bigint_max +FROM pg_sequences WHERE sequencename = 'standalone_seq'; + max_is_bigint_max +------------------- + t +(1 row) + +SELECT pg_catalog.setval('standalone_seq', 4446196691613229056, true); + setval +--------------------- + 4446196691613229056 +(1 row) + +-- ---------------------------------------------------------------------- +-- Cleanup +-- ---------------------------------------------------------------------- +DROP TABLE orders CASCADE; +DROP SEQUENCE standalone_seq; +DROP EXTENSION snowflake; diff --git a/snowflake--1.2--2.0.sql b/snowflake--1.2--2.0.sql index ee0289c..66588de 100644 --- a/snowflake--1.2--2.0.sql +++ b/snowflake--1.2--2.0.sql @@ -100,7 +100,6 @@ DECLARE v_seq record; v_cmd text; v_num_altered integer := 0; - v_last_value bigint; BEGIN -- ---- -- We are looking for column defaults that use the requested @@ -175,25 +174,17 @@ BEGIN END LOOP; -- ---- - -- If we found something, we need to change the sequence itself to - -- settings that prevent pg_catalog.nextval() from working. We do this - -- by setting the sequence's MAXVAL to its current last_value + 1, - -- then invoke our own nextval() function to bump it. + -- If we found something, bump the sequence past its current + -- last_value by invoking snowflake.nextval(). The MAXVALUE is + -- left at the full bigint range (2^63-1) so subsequent + -- snowflake IDs - and the pg_catalog.setval() calls that pg_dump + -- emits during a restore - all stay within bounds. -- ---- IF v_num_altered > 0 THEN - v_cmd = 'SELECT last_value FROM ' || - pg_catalog.quote_ident(N.nspname) || '.' || - pg_catalog.quote_ident(C.relname) - FROM pg_catalog.pg_class C - JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace - WHERE C.oid = p_seqid; - EXECUTE v_cmd INTO v_last_value; - v_cmd = 'ALTER SEQUENCE ' || pg_catalog.quote_ident(N.nspname) || '.' || pg_catalog.quote_ident(C.relname) || - ' NO CYCLE MAXVALUE ' || - v_last_value + 1 + ' AS bigint NO CYCLE MAXVALUE 9223372036854775807' FROM pg_catalog.pg_class C JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace WHERE C.oid = p_seqid; diff --git a/snowflake--2.0--2.2.sql b/snowflake--2.0--2.2.sql index c338b61..d1437c8 100644 --- a/snowflake--2.0--2.2.sql +++ b/snowflake--2.0--2.2.sql @@ -20,7 +20,6 @@ DECLARE v_seq record; v_cmd text; v_num_altered integer := 0; - v_last_value bigint; v_seqname1 text; v_seqname2 text; @@ -109,24 +108,16 @@ BEGIN END LOOP; -- ---- - -- Finally we need to change the sequence itself to settings that - -- prevent pg_catalog.nextval() from working. We do this by setting - -- the sequence's MAXVAL to its current last_value + 1, then invoke - -- our own nextval() function to bump it. + -- Finally bump the sequence past its current last_value by + -- invoking snowflake.nextval(). The MAXVALUE is left at the + -- full bigint range (2^63-1) so subsequent snowflake IDs - and + -- the pg_catalog.setval() calls that pg_dump emits during a + -- restore - all stay within bounds. -- ---- - v_cmd = 'SELECT last_value FROM ' || - pg_catalog.quote_ident(N.nspname) || '.' || - pg_catalog.quote_ident(C.relname) - FROM pg_catalog.pg_class C - JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace - WHERE C.oid = p_seqid; - EXECUTE v_cmd INTO v_last_value; - v_cmd = 'ALTER SEQUENCE ' || pg_catalog.quote_ident(N.nspname) || '.' || pg_catalog.quote_ident(C.relname) || - ' NO CYCLE MAXVALUE ' || - v_last_value + 1 + ' AS bigint NO CYCLE MAXVALUE 9223372036854775807' FROM pg_catalog.pg_class C JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace WHERE C.oid = p_seqid; diff --git a/snowflake--2.0.sql b/snowflake--2.0.sql index 1e9546b..fe87e61 100644 --- a/snowflake--2.0.sql +++ b/snowflake--2.0.sql @@ -145,7 +145,6 @@ DECLARE v_seq record; v_cmd text; v_num_altered integer := 0; - v_last_value bigint; BEGIN -- ---- -- We are looking for column defaults that use the requested @@ -220,25 +219,17 @@ BEGIN END LOOP; -- ---- - -- If we found something, we need to change the sequence itself to - -- settings that prevent pg_catalog.nextval() from working. We do this - -- by setting the sequence's MAXVAL to its current last_value + 1, - -- then invoke our own nextval() function to bump it. + -- If we found something, bump the sequence past its current + -- last_value by invoking snowflake.nextval(). The MAXVALUE is + -- left at the full bigint range (2^63-1) so subsequent + -- snowflake IDs - and the pg_catalog.setval() calls that pg_dump + -- emits during a restore - all stay within bounds. -- ---- IF v_num_altered > 0 THEN - v_cmd = 'SELECT last_value FROM ' || - pg_catalog.quote_ident(N.nspname) || '.' || - pg_catalog.quote_ident(C.relname) - FROM pg_catalog.pg_class C - JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace - WHERE C.oid = p_seqid; - EXECUTE v_cmd INTO v_last_value; - v_cmd = 'ALTER SEQUENCE ' || pg_catalog.quote_ident(N.nspname) || '.' || pg_catalog.quote_ident(C.relname) || - ' NO CYCLE MAXVALUE ' || - v_last_value + 1 + ' AS bigint NO CYCLE MAXVALUE 9223372036854775807' FROM pg_catalog.pg_class C JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace WHERE C.oid = p_seqid; diff --git a/snowflake--2.2--2.3.sql b/snowflake--2.2--2.3.sql index 3a431e5..103871c 100644 --- a/snowflake--2.2--2.3.sql +++ b/snowflake--2.2--2.3.sql @@ -24,7 +24,6 @@ DECLARE v_attrdef record; v_cmd text; v_num_altered integer; - v_last_value bigint; v_seqname1 text; extseqname text; @@ -133,24 +132,16 @@ BEGIN EXECUTE v_cmd; -- ---- - -- Finally we need to change the sequence itself to settings that - -- prevent pg_catalog.nextval() from working. We do this by setting - -- the sequence's MAXVAL to its current last_value + 1, then invoke - -- our own nextval() function to bump it. + -- Finally bump the sequence past its current last_value by + -- invoking snowflake.nextval(). The MAXVALUE is left at the + -- full bigint range (2^63-1) so subsequent snowflake IDs - and + -- the pg_catalog.setval() calls that pg_dump emits during a + -- restore - all stay within bounds. -- ---- - v_cmd = 'SELECT last_value FROM ' || - pg_catalog.quote_ident(N.nspname) || '.' || - pg_catalog.quote_ident(C.relname) - FROM pg_catalog.pg_class C - JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace - WHERE C.oid = p_seqid; - EXECUTE v_cmd INTO v_last_value; - v_cmd = 'ALTER SEQUENCE ' || pg_catalog.quote_ident(N.nspname) || '.' || pg_catalog.quote_ident(C.relname) || - ' NO CYCLE MAXVALUE ' || - v_last_value + 1 + ' AS bigint NO CYCLE MAXVALUE 9223372036854775807' FROM pg_catalog.pg_class C JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace WHERE C.oid = p_seqid; diff --git a/snowflake--2.2.sql b/snowflake--2.2.sql index 3a9db9b..5488302 100644 --- a/snowflake--2.2.sql +++ b/snowflake--2.2.sql @@ -146,7 +146,6 @@ DECLARE v_seq record; v_cmd text; v_num_altered integer := 0; - v_last_value bigint; v_seqname1 text; v_seqname2 text; @@ -235,24 +234,16 @@ BEGIN END LOOP; -- ---- - -- Finally we need to change the sequence itself to settings that - -- prevent pg_catalog.nextval() from working. We do this by setting - -- the sequence's MAXVAL to its current last_value + 1, then invoke - -- our own nextval() function to bump it. + -- Finally bump the sequence past its current last_value by + -- invoking snowflake.nextval(). The MAXVALUE is left at the + -- full bigint range (2^63-1) so subsequent snowflake IDs - and + -- the pg_catalog.setval() calls that pg_dump emits during a + -- restore - all stay within bounds. -- ---- - v_cmd = 'SELECT last_value FROM ' || - pg_catalog.quote_ident(N.nspname) || '.' || - pg_catalog.quote_ident(C.relname) - FROM pg_catalog.pg_class C - JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace - WHERE C.oid = p_seqid; - EXECUTE v_cmd INTO v_last_value; - v_cmd = 'ALTER SEQUENCE ' || pg_catalog.quote_ident(N.nspname) || '.' || pg_catalog.quote_ident(C.relname) || - ' NO CYCLE MAXVALUE ' || - v_last_value + 1 + ' AS bigint NO CYCLE MAXVALUE 9223372036854775807' FROM pg_catalog.pg_class C JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace WHERE C.oid = p_seqid; diff --git a/snowflake--2.3--2.4.sql b/snowflake--2.3--2.4.sql index 80cd6d5..24955bd 100644 --- a/snowflake--2.3--2.4.sql +++ b/snowflake--2.3--2.4.sql @@ -32,7 +32,6 @@ DECLARE v_seq record; v_cmd text; v_num_altered integer := 0; - v_last_value bigint; v_seqname1 text; v_extseqname text; v_is_serial_def boolean; @@ -216,24 +215,16 @@ BEGIN -- sequences used explicitly by applications -- ---- - -- Finally we need to change the sequence itself to settings that - -- prevent pg_catalog.nextval() from working. We do this by setting - -- the sequence's MAXVAL to its current last_value + 1, then invoke - -- our own nextval() function to bump it. + -- Finally bump the sequence past its current last_value by + -- invoking snowflake.nextval(). The MAXVALUE is left at the + -- full bigint range (2^63-1) so subsequent snowflake IDs - and + -- the pg_catalog.setval() calls that pg_dump emits during a + -- restore - all stay within bounds. -- ---- - v_cmd = 'SELECT last_value FROM ' || - pg_catalog.quote_ident(N.nspname) || '.' || - pg_catalog.quote_ident(C.relname) - FROM pg_catalog.pg_class C - JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace - WHERE C.oid = p_seqid; - EXECUTE v_cmd INTO v_last_value; - v_cmd = 'ALTER SEQUENCE ' || pg_catalog.quote_ident(N.nspname) || '.' || pg_catalog.quote_ident(C.relname) || - ' NO CYCLE MAXVALUE ' || - v_last_value + 1 + ' AS bigint NO CYCLE MAXVALUE 9223372036854775807' FROM pg_catalog.pg_class C JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace WHERE C.oid = p_seqid; diff --git a/snowflake--2.3.sql b/snowflake--2.3.sql index ebe5cb7..7e2c270 100644 --- a/snowflake--2.3.sql +++ b/snowflake--2.3.sql @@ -150,7 +150,6 @@ DECLARE v_attrdef record; v_cmd text; v_num_altered integer; - v_last_value bigint; v_seqname1 text; extseqname text; @@ -259,24 +258,16 @@ BEGIN EXECUTE v_cmd; -- ---- - -- Finally we need to change the sequence itself to settings that - -- prevent pg_catalog.nextval() from working. We do this by setting - -- the sequence's MAXVAL to its current last_value + 1, then invoke - -- our own nextval() function to bump it. + -- Finally bump the sequence past its current last_value by + -- invoking snowflake.nextval(). The MAXVALUE is left at the + -- full bigint range (2^63-1) so subsequent snowflake IDs - and + -- the pg_catalog.setval() calls that pg_dump emits during a + -- restore - all stay within bounds. -- ---- - v_cmd = 'SELECT last_value FROM ' || - pg_catalog.quote_ident(N.nspname) || '.' || - pg_catalog.quote_ident(C.relname) - FROM pg_catalog.pg_class C - JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace - WHERE C.oid = p_seqid; - EXECUTE v_cmd INTO v_last_value; - v_cmd = 'ALTER SEQUENCE ' || pg_catalog.quote_ident(N.nspname) || '.' || pg_catalog.quote_ident(C.relname) || - ' NO CYCLE MAXVALUE ' || - v_last_value + 1 + ' AS bigint NO CYCLE MAXVALUE 9223372036854775807' FROM pg_catalog.pg_class C JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace WHERE C.oid = p_seqid; diff --git a/snowflake--2.4--2.5.sql b/snowflake--2.4--2.5.sql new file mode 100644 index 0000000..d953515 --- /dev/null +++ b/snowflake--2.4--2.5.sql @@ -0,0 +1,313 @@ +/* snowflake--2.4--2.5.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION snowflake UPDATE TO '2.5'" to load this file. \quit + +-- ---------------------------------------------------------------------- +-- 2.4 -> 2.5 - fix MAXVALUE on snowflake-converted sequences. +-- +-- The pre-2.5 convert_sequence_to_snowflake() set MAXVALUE to +-- (old last_value + 1) when converting a sequence to snowflake IDs. +-- The intent was to "lock out" pg_catalog.nextval(), but +-- snowflake.nextval() bypasses MAXVALUE so the sequence happily +-- generated huge snowflake IDs while the catalog still reported a +-- tiny ceiling. pg_dump captures both ("MAXVALUE 4" plus +-- "SELECT pg_catalog.setval(..., 4446196691613229056, true)") and +-- the resulting dump cannot be restored: +-- +-- ERROR: setval: value 4446196691613229056 is out of bounds for +-- sequence "orders_id_seq" (1..4) +-- +-- 2.5 changes the sequence's data type to bigint and sets MAXVALUE to +-- the full bigint range (2^63-1) so any snowflake ID and any +-- pg_dump-emitted setval stays in bounds. This script: +-- 1. Re-installs convert_sequence_to_snowflake() with the fix so +-- future conversions store the correct MAXVALUE. +-- 2. Walks pg_sequences and raises MAXVALUE to 2^63-1 (and the +-- sequence data type to bigint) on any sequence whose column +-- default already uses snowflake.nextval() and whose max_value is +-- below bigint MAX. +-- ---------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION snowflake.convert_sequence_to_snowflake(p_seqid regclass) +RETURNS integer +SET search_path = pg_catalog +AS $$ +DECLARE + v_objdesc record; -- contains target (reloid,attnum) value + v_identdesc record; -- sequence-related flags (attisidentity) + v_attrdef record; + v_attr record; + v_seq record; + v_cmd text; + v_num_altered integer := 0; + v_seqname1 text; + v_extseqname text; + v_is_serial_def boolean; + v_textstr text; +BEGIN + -- Identify the (relation,attnum) that uses this sequence as a source + -- for values. Follow the logic of the getOwnedSequences_internal. + -- + -- Complain, if such data wasn't found - incoming object may be not + -- a sequence, or sequence which is used for different purposes. + -- v_objdesc's fields: + -- heapreloid - Oid of the target relation. + -- nspname - namespace of the sequence + -- seqname - sequence name + -- attnum - number of relation's attribute that employs this sequence + SELECT INTO v_objdesc + refobjid AS heapreloid, + c.relnamespace::regnamespace::text AS nspname, + c.relname AS seqname, + refobjsubid AS attnum, + deptype + FROM pg_depend AS d JOIN pg_class AS c ON (d.objid = c.oid) + WHERE + classid = 'pg_class'::regclass AND + deptype IN ('a','i','n') AND + c.oid = p_seqid AND relkind = 'S' + ORDER BY deptype; -- prioritize (a)uto and (i)nternal before (n)ormal + + IF (v_objdesc IS NULL) THEN + raise EXCEPTION 'Input value "%" is not a valid convertable sequence', p_seqid; + RETURN false; + END IF; + + -- ---- + -- We are looking for column defaults that use the requested + -- sequence and the function nextval(). Because pg_get_expr() + -- omits the schemaname of the sequence if it is "public" we + -- need to be prepared for a schema qualified and unqualified + -- name here. + -- ---- + SELECT INTO v_seqname1 + quote_ident(v_objdesc.seqname); + SELECT INTO v_extseqname + quote_ident(v_objdesc.nspname) || '.' || quote_ident(v_objdesc.seqname); + + IF v_objdesc.deptype IN ('a','i') THEN + -- Handle IDENTITY case and SERIAL/BIGSERIAL case + + SELECT INTO v_identdesc + (attidentity = 'a' OR attidentity = 'd') AS is_identity, + c.relnamespace::regnamespace::text AS nspname, + c.relname AS relname, + a.attname AS attname + FROM pg_attribute a JOIN pg_class c ON (c.oid = a.attrelid) + WHERE a.attrelid = v_objdesc.heapreloid AND a.attnum = v_objdesc.attnum; + + IF (v_identdesc.is_identity) THEN + UPDATE pg_attribute SET attidentity = '' + WHERE attrelid = v_objdesc.heapreloid AND attnum = v_objdesc.attnum; + RAISE NOTICE + 'Update pg_attribute: reset attidentity value for table %, column %', + v_objdesc.heapreloid::regclass, v_identdesc.attname; + ELSE + + -- Extract DEFAULT definition for the column and conver it into + -- a readable string representation. + SELECT INTO v_textstr + pg_get_expr(ad.adbin, ad.adrelid, true) + FROM pg_attrdef ad + WHERE adrelid = v_objdesc.heapreloid AND adnum = v_objdesc.attnum; + + -- Check that the DEFAULT expression contains input sequence in a form + -- as related to the serial and bigserial type. + SELECT INTO v_is_serial_def + CASE WHEN + v_textstr = 'nextval(' || quote_literal(v_seqname1) || '::regclass)' OR + v_textstr = 'nextval(' || quote_literal(v_extseqname) || '::regclass)' + THEN true ELSE false END; + + -- If there is another DEFAULT expression already set for this column, we + -- are not so bold to remove it, just complain, + IF (NOT v_is_serial_def) THEN + raise EXCEPTION + 'definition of DEFAULT value for column "%" of relation "%" does not correspond serial or bigserial type: "%"', + v_identdesc.attname, v_identdesc.relname, v_textstr; + END IF; + END IF; + + -- ---- + -- If the attribute type is not bigint, we change it + -- ---- + v_num_altered = + snowflake.convert_column_to_int8(v_objdesc.heapreloid::regclass, + v_objdesc.attnum::smallint); + + -- ---- + -- Now we can change the default to snowflake.nextval() + -- ---- + v_cmd = 'ALTER TABLE ' || + quote_ident(v_identdesc.nspname) || '.' || + quote_ident(v_identdesc.relname) || + ' ALTER COLUMN ' || quote_ident(v_identdesc.attname) || + ' SET DEFAULT snowflake.nextval(' || + quote_literal( + quote_ident(v_objdesc.nspname) || '.' || + quote_ident(v_objdesc.seqname) + ) || '::regclass)'; + RAISE NOTICE 'EXECUTE %', v_cmd; + EXECUTE v_cmd; + ELSE + -- Look for cases where the sequence is used as an explicit default value + + FOR v_attrdef IN + WITH AD AS ( + SELECT AD.*, + pg_get_expr(AD.adbin, AD.adrelid, true) adstr + FROM pg_attrdef AD + ) + SELECT * FROM AD + WHERE AD.adstr = 'nextval(' || quote_literal(v_seqname1) || '::regclass)' + OR AD.adstr = 'nextval(' || quote_literal(v_extseqname) || '::regclass)' + LOOP + -- ---- + -- Get the attribute definition + -- ---- + SELECT * INTO v_attr + FROM pg_namespace N + JOIN pg_class C + ON N.oid = C.relnamespace + JOIN pg_attribute A + ON C.oid = A.attrelid + WHERE A.attrelid = v_attrdef.adrelid + AND A.attnum = v_attrdef.adnum; + + IF NOT FOUND THEN + RAISE EXCEPTION 'Attribute for % not found', v_attrdef.adstr; + END IF; + + -- ---- + -- Get the sequence definition + -- ---- + SELECT * INTO v_seq + FROM pg_namespace N + JOIN pg_class C + ON N.oid = C.relnamespace + WHERE C.oid = p_seqid; + + IF NOT FOUND THEN + RAISE EXCEPTION 'Sequence with Oid % not found', p_seqid; + END IF; + + -- ---- + -- If the attribute type is not bigint, we change it + -- ---- + v_num_altered = v_num_altered + + snowflake.convert_column_to_int8(v_attr.attrelid, v_attr.attnum); + + -- ---- + -- Now we can change the default to snowflake.nextval() + -- ---- + v_cmd = 'ALTER TABLE ' || + quote_ident(v_attr.nspname) || '.' || + quote_ident(v_attr.relname) || + ' ALTER COLUMN ' || + quote_ident(v_attr.attname) || + ' SET DEFAULT snowflake.nextval(' || + quote_literal( + quote_ident(v_seq.nspname) || '.' || + quote_ident(v_seq.relname) + ) || + '::regclass)'; + RAISE NOTICE 'EXECUTE %', v_cmd; + EXECUTE v_cmd; + + v_num_altered = v_num_altered + 1; + END LOOP; + END IF; + + -- Note: for plain sequences not associated with columns, we still + -- fall through to here so snowflake can be used, to support + -- sequences used explicitly by applications + + -- ---- + -- Finally bump the sequence past its current last_value by + -- invoking snowflake.nextval(). The MAXVALUE is left at the + -- full bigint range (2^63-1) so subsequent snowflake IDs - and + -- the pg_catalog.setval() calls that pg_dump emits during a + -- restore - all stay within bounds. + -- ---- + v_cmd = 'ALTER SEQUENCE ' || + pg_catalog.quote_ident(N.nspname) || '.' || + pg_catalog.quote_ident(C.relname) || + ' AS bigint NO CYCLE MAXVALUE 9223372036854775807' + FROM pg_catalog.pg_class C + JOIN pg_catalog.pg_namespace N ON N.oid = C.relnamespace + WHERE C.oid = p_seqid; + RAISE NOTICE '%', v_cmd; + EXECUTE v_cmd; + + PERFORM snowflake.nextval(p_seqid); + RETURN v_num_altered; +END; +$$ LANGUAGE plpgsql; + +-- ---------------------------------------------------------------------- +-- One-shot repair: raise MAXVALUE to 2^63-1 on every sequence that was +-- already converted to snowflake but still has the old, low ceiling. +-- +-- convert_sequence_to_snowflake() supports three flavors of sequence +-- and the repair has to cover all three: +-- 1. SERIAL / BIGSERIAL / IDENTITY (pg_depend.deptype = 'a' or 'i') +-- 2. Plain sequence used via an explicit nextval() column DEFAULT +-- (pg_depend.deptype = 'n') +-- 3. Standalone sequence with no associated column, used directly +-- via snowflake.nextval('seq_name'). +-- +-- Flavors (1) and (2) are detected by the column DEFAULT being rewritten +-- to snowflake.nextval(...). Flavor (3) leaves no such marker on a +-- column, so we detect it by the inconsistent on-disk state the bug +-- produces: last_value > max_value, which cannot occur in normal +-- PostgreSQL operation and is the unique signature of a pre-2.5 +-- conversion of a standalone sequence. +-- ---------------------------------------------------------------------- +DO $repair$ +DECLARE + r record; + v_cmd text; +BEGIN + FOR r IN + -- Flavors 1 + 2: owned or explicit-default sequences whose + -- column DEFAULT now references snowflake.nextval(...) + SELECT n.nspname AS seq_nspname, + c.relname AS seq_relname, + s.max_value + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN pg_sequences s ON s.schemaname = n.nspname + AND s.sequencename = c.relname + JOIN pg_depend d ON d.objid = c.oid + AND d.classid = 'pg_class'::regclass + AND d.deptype IN ('a','i','n') + JOIN pg_attrdef ad ON ad.adrelid = d.refobjid + AND ad.adnum = d.refobjsubid + WHERE c.relkind = 'S' + AND s.max_value < 9223372036854775807 + AND pg_get_expr(ad.adbin, ad.adrelid, true) LIKE '%snowflake.nextval(%' + UNION + -- Flavor 3: standalone sequence with no rewritten column DEFAULT. + -- last_value > max_value is the unique signature of a pre-2.5 + -- conversion (snowflake.nextval bypasses MAXVALUE, so the value + -- stored is well beyond the recorded ceiling). + SELECT n.nspname AS seq_nspname, + c.relname AS seq_relname, + s.max_value + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN pg_sequences s ON s.schemaname = n.nspname + AND s.sequencename = c.relname + WHERE c.relkind = 'S' + AND s.max_value < 9223372036854775807 + AND s.last_value IS NOT NULL + AND s.last_value > s.max_value + LOOP + v_cmd := format('ALTER SEQUENCE %I.%I AS bigint NO CYCLE MAXVALUE 9223372036854775807', + r.seq_nspname, r.seq_relname); + RAISE NOTICE 'snowflake 2.4->2.5 repair: % (was max_value=%)', v_cmd, r.max_value; + EXECUTE v_cmd; + END LOOP; +END +$repair$; diff --git a/snowflake.control b/snowflake.control index c59529b..4582083 100644 --- a/snowflake.control +++ b/snowflake.control @@ -1,6 +1,6 @@ # snowflake extension comment = 'Snowflake style IDs for PostgreSQL' -default_version = '2.4' +default_version = '2.5' module_pathname = '$libdir/snowflake' relocatable = false schema = snowflake diff --git a/sql/maxvalue.sql b/sql/maxvalue.sql new file mode 100644 index 0000000..bbde82e --- /dev/null +++ b/sql/maxvalue.sql @@ -0,0 +1,68 @@ +/* + * maxvalue.sql - regression for the SUP-140 dump/restore failure. + * + * Before the fix, convert_sequence_to_snowflake() set + * ALTER SEQUENCE ... MAXVALUE (last_value + 1) + * which is fine for the snowflake.nextval() path (it bypasses MAXVALUE) + * but fatal for pg_dump/restore: the dump captures the resulting + * snowflake-sized last_value via pg_catalog.setval(...) and the + * restore is rejected with + * ERROR: setval: value is out of bounds for + * sequence "" (1..) + * + * This test asserts that after conversion the sequence's max_value is + * the full bigint range (2^63-1). + */ + +\set VERBOSITY terse + +SET snowflake.node = 1; + +CREATE EXTENSION snowflake; + +-- ---------------------------------------------------------------------- +-- 1. Customer reproducer: bigserial table, INSERT, convert. +-- ---------------------------------------------------------------------- +CREATE TABLE orders ( + id bigserial PRIMARY KEY, + customer text NOT NULL, + amount numeric NOT NULL +); +INSERT INTO orders (customer, amount) VALUES + ('Alice', 100.00), + ('Bob', 250.00), + ('Carol', 75.50); + +SELECT snowflake.convert_sequence_to_snowflake('orders_id_seq'::regclass); + +-- After conversion max_value MUST be the bigint ceiling, not (last_value+1). +SELECT max_value = 9223372036854775807 AS max_is_bigint_max +FROM pg_sequences WHERE sequencename = 'orders_id_seq'; + +-- ---------------------------------------------------------------------- +-- 2. Generate a real snowflake-sized id; setval to that value must succeed +-- (this is what pg_dump emits in the restore script). +-- ---------------------------------------------------------------------- +INSERT INTO orders (customer, amount) VALUES ('Dave', 999) RETURNING id > 9999; + +-- A pg_catalog.setval() with a snowflake-sized value must NOT raise +-- "out of bounds for sequence" any more. +SELECT pg_catalog.setval('orders_id_seq', 4446196691613229056, true); + +SELECT last_value FROM orders_id_seq; + +-- ---------------------------------------------------------------------- +-- 3. Plain (un-owned) sequence path. +-- ---------------------------------------------------------------------- +CREATE SEQUENCE standalone_seq START 100; +SELECT snowflake.convert_sequence_to_snowflake('standalone_seq'::regclass); +SELECT max_value = 9223372036854775807 AS max_is_bigint_max +FROM pg_sequences WHERE sequencename = 'standalone_seq'; +SELECT pg_catalog.setval('standalone_seq', 4446196691613229056, true); + +-- ---------------------------------------------------------------------- +-- Cleanup +-- ---------------------------------------------------------------------- +DROP TABLE orders CASCADE; +DROP SEQUENCE standalone_seq; +DROP EXTENSION snowflake;