diff --git a/.github/workflows/build-cloudberry-rocky8.yml b/.github/workflows/build-cloudberry-rocky8.yml index 4986eae11b2..7ae0da0aa82 100644 --- a/.github/workflows/build-cloudberry-rocky8.yml +++ b/.github/workflows/build-cloudberry-rocky8.yml @@ -310,6 +310,7 @@ jobs: "contrib/pgcrypto:installcheck", "contrib/pgstattuple:installcheck", "contrib/tablefunc:installcheck", + "contrib/try_convert:installcheck", "contrib/passwordcheck:installcheck", "contrib/pg_buffercache:installcheck", "contrib/sslinfo:installcheck"] diff --git a/.github/workflows/build-cloudberry.yml b/.github/workflows/build-cloudberry.yml index 0d76fa0da51..8cb0cbb2665 100644 --- a/.github/workflows/build-cloudberry.yml +++ b/.github/workflows/build-cloudberry.yml @@ -308,6 +308,7 @@ jobs: "contrib/pgcrypto:installcheck", "contrib/pgstattuple:installcheck", "contrib/tablefunc:installcheck", + "contrib/try_convert:installcheck", "contrib/passwordcheck:installcheck", "contrib/pg_buffercache:installcheck", "contrib/sslinfo:installcheck"] diff --git a/.github/workflows/build-deb-cloudberry-ubuntu24.04.yml b/.github/workflows/build-deb-cloudberry-ubuntu24.04.yml index 041eabc252b..072a0e77258 100644 --- a/.github/workflows/build-deb-cloudberry-ubuntu24.04.yml +++ b/.github/workflows/build-deb-cloudberry-ubuntu24.04.yml @@ -249,6 +249,7 @@ jobs: "contrib/pgcrypto:installcheck", "contrib/pgstattuple:installcheck", "contrib/tablefunc:installcheck", + "contrib/try_convert:installcheck", "contrib/passwordcheck:installcheck", "contrib/pg_buffercache:installcheck", "contrib/sslinfo:installcheck"] diff --git a/.github/workflows/build-deb-cloudberry.yml b/.github/workflows/build-deb-cloudberry.yml index f8eadee3c8f..b01eb2a1385 100644 --- a/.github/workflows/build-deb-cloudberry.yml +++ b/.github/workflows/build-deb-cloudberry.yml @@ -242,6 +242,7 @@ jobs: "contrib/pgcrypto:installcheck", "contrib/pgstattuple:installcheck", "contrib/tablefunc:installcheck", + "contrib/try_convert:installcheck", "contrib/passwordcheck:installcheck", "contrib/pg_buffercache:installcheck", "contrib/sslinfo:installcheck"] diff --git a/GNUmakefile.in b/GNUmakefile.in index 70f635b16e7..50d3be68507 100644 --- a/GNUmakefile.in +++ b/GNUmakefile.in @@ -27,6 +27,7 @@ all: $(MAKE) -C contrib/btree_gin all $(MAKE) -C contrib/pg_trgm all $(MAKE) -C contrib/tablefunc all + $(MAKE) -C contrib/try_convert all $(MAKE) -C contrib/passwordcheck all $(MAKE) -C contrib/pg_buffercache all ifeq ($(with_openssl), yes) @@ -77,6 +78,7 @@ install: $(MAKE) -C contrib/btree_gin $@ $(MAKE) -C contrib/pg_trgm $@ $(MAKE) -C contrib/tablefunc $@ + $(MAKE) -C contrib/try_convert $@ $(MAKE) -C contrib/passwordcheck $@ $(MAKE) -C contrib/pg_buffercache $@ ifeq ($(enable_pax), yes) diff --git a/contrib/Makefile b/contrib/Makefile index b14600e3557..5ea76366363 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -50,6 +50,7 @@ SUBDIRS = \ tablefunc \ tcn \ test_decoding \ + try_convert \ tsm_system_rows \ tsm_system_time \ unaccent \ diff --git a/contrib/try_convert/.gitignore b/contrib/try_convert/.gitignore new file mode 100644 index 00000000000..30d79857f6c --- /dev/null +++ b/contrib/try_convert/.gitignore @@ -0,0 +1,6 @@ +# Generated subdirectories +/results/ +/sql/ +/expected/ +/input/ +/output/ \ No newline at end of file diff --git a/contrib/try_convert/Makefile b/contrib/try_convert/Makefile new file mode 100644 index 00000000000..c30bfca155e --- /dev/null +++ b/contrib/try_convert/Makefile @@ -0,0 +1,52 @@ +# contrib/try_convert/Makefile + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +MODULE_big = try_convert +OBJS = try_convert.o $(WIN32RES) + +EXTENSION = try_convert +DATA = try_convert--1.0.sql +PGFILEDESC = "try_convert - function for type cast" + +# The test cases are derived from the catalog files and from the sample values +# in data/, so they are generated by generate-tests rather than stored in the +# tree, together with the sql/ and expected/ files pg_regress derives from them. +REGRESS = try_convert +REGRESS_PREP = generate-tests +EXTRA_CLEAN = input output sql expected results + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = contrib/try_convert +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +.PHONY: generate-tests +generate-tests: + $(MKDIR_P) input output + python3 scripts/generate_test.py + +.PHONY: verify +verify: generate-tests + python3 scripts/verify.py diff --git a/contrib/try_convert/README.md b/contrib/try_convert/README.md new file mode 100644 index 00000000000..e1f20d9bf5f --- /dev/null +++ b/contrib/try_convert/README.md @@ -0,0 +1,116 @@ +# TRY_CONVERT + +TRY_CONVERT is Greenplum/Cloudberry extension, which adds function for error-safe type cast like [TRY_CAST from SQL-Server](https://learn.microsoft.com/ru-ru/sql/t-sql/functions/try-cast-transact-sql?view=sql-server-ver16) + +## Usage + +``` +TRY_CONVERT(SOURCE_VALUE, DEFAULT_VALUE::TARGET_TYPE) + returns (VALUE_IN_TARGET_TYPE or DEFAULT_VALUE) +``` + +``` +TRY_CONVERT('42'::text, NULL::int2) -- returns 42::int2 +TRY_CONVERT('42d'::text, NULL::int2) -- returns NULL::int2 +TRY_CONVERT('42d'::text, 1234::int2) -- returns 1234::int2 +``` + +### Extension's type casts + +Casting from extensions types is able only for extensions: + +- hstore +- citext + +To enable casting from hstore and citext types use, `add_type_for_try_convert(regtype)` function + +## Error handling + +The cast is executed inside a `PG_TRY()` block: when the cast function reports +a failure, the error is discarded and the default value is returned instead. +Query cancellation and assertion failures are never swallowed, they are +re-thrown, the same way plpgsql handles `EXCEPTION WHEN others`. + +The long-term plan is to replace the `PG_TRY()` block by the "soft" error +handling concept introduced in Postgres 17 (https://github.com/postgres/postgres/commit/ccff2d20ed9622815df2a7deffce8a7b14830965), +which lets a datatype input function report a conversion failure without +throwing. That concept was spread on data types in [21be368 +Preview](https://github.com/open-gpdb/gpdb/commit/21be3688729ec4468ffd083da197721860fa2cbd) and [d31f362 +](https://github.com/open-gpdb/gpdb/commit/d31f362250105e456961c2c9249693e42e67eca9) commits. +It requires converting the datatype input functions of Cloudberry first. + +## Why signature is so strange? + +Greenplum/Cloudberry function polymorphism accept to have polymorphic functions only one any type in signature. + +## Supported casts + + ✅ Values Cast + ✅ Types with typemod + ❌ Array-Array Cast + ❌ To Domain type cast + +An unsupported or non-existing cast is a query error, it is not turned into the +default value: only failures caused by the converted data are. + +## Tests + +The regression test is generated out of the catalog files and of the sample +values in `data/`, so it is not stored in the repository. `make installcheck` +generates it into `input/` and `output/` and then runs it: + +``` +make -C contrib/try_convert installcheck +``` + +`make -C contrib/try_convert generate-tests` generates it without running it. + +## Benchmark results by pgbench + + +| | without errors | with errors | +| --- | --- | --- | +| cast | 299.346 | ❌ fails | +| try_convert | 984.280 | 1004.524 | +| sql | 1384.784 | 5787.115 | +| sql execute | 5843.220 | 5898.813 | + + +SQL version: +``` +CREATE OR REPLACE FUNCTION try_convert_into_int(_in text, d int2) RETURNS int2 + LANGUAGE plpgsql AS +$func$ + BEGIN + RETURN CAST(_in AS int2); + EXCEPTION WHEN others THEN + RETURN d; + END +$func$; +``` + + +SQL with execute version: +``` +CREATE OR REPLACE FUNCTION try_convert_by_sql(_in text, INOUT _out ANYELEMENT) + LANGUAGE plpgsql AS +$func$ +BEGIN + EXECUTE format('SELECT %L::%s', $1, pg_typeof(_out)) + INTO _out; +EXCEPTION WHEN others THEN + -- do nothing: _out already carries default +END +$func$; +``` + +Data: +``` +drop table if exists text_ints; create table text_ints (n text); +Insert into text_ints(n) select (random()*1000)::int4::text from generate_series(1,1000000); + +drop table if exists text_error_ints; create table text_error_ints (n text); +Insert into text_error_ints(n) select (random()*1000000)::int8::text from generate_series(1,1000000); +``` + + diff --git a/contrib/try_convert/data/corr_date.data b/contrib/try_convert/data/corr_date.data new file mode 100644 index 00000000000..d11addc0793 --- /dev/null +++ b/contrib/try_convert/data/corr_date.data @@ -0,0 +1,40 @@ +13:35:45 +13:35:90 +13:121:45 +2323:35:45 +13:35:-45 +13:-35:45 +-13:35:45 + +12:52:43 AM +15:52:43 AM +12:152:43 AM +12:52:143 AM +12:52:43 PM +15:52:43 PM +12:152:43 PM +12:52:143 PM + +21:52:07 JST +21:52:07 MSSSSSK + +21:52:07+03:02 +21:52:07+33:02 +21:52:07-33:02 +21:52:07+03:222 + +1982-06-27 18:52:43 +1982-06-32 18:52:43 +1982-32-27 18:52:43 +99999999999999-06-27 18:52:43 + +1982-12-12 01:01:59+09:00 +1982-12-12 01:01:59+29:00 + +1982-06-27 +1982-06-32 +1982-32-27 +999999999999999-06-27 + +11-30-0002 BC +11-30-200000000 BC \ No newline at end of file diff --git a/contrib/try_convert/data/corr_float4.data b/contrib/try_convert/data/corr_float4.data new file mode 100644 index 00000000000..3d3ada99f23 --- /dev/null +++ b/contrib/try_convert/data/corr_float4.data @@ -0,0 +1,17 @@ +1 +2 +333333333 +333333333333333333333 +5555555555555555555555555555555555555555555 +dfgdg435fw2342rf445 +6.6332342343243242342342342342343243244324 +354345345345435345345345345.23467567867325345345 +$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7 +Twenty-two +12234441231.13123123 +485728435843759345135.4324234234 +4823849249230.f +19203234723472-e102 +0000000000000.00000000000000000001 +234324+e4443 \ No newline at end of file diff --git a/contrib/try_convert/data/corr_float8.data b/contrib/try_convert/data/corr_float8.data new file mode 100644 index 00000000000..3d3ada99f23 --- /dev/null +++ b/contrib/try_convert/data/corr_float8.data @@ -0,0 +1,17 @@ +1 +2 +333333333 +333333333333333333333 +5555555555555555555555555555555555555555555 +dfgdg435fw2342rf445 +6.6332342343243242342342342342343243244324 +354345345345435345345345345.23467567867325345345 +$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7 +Twenty-two +12234441231.13123123 +485728435843759345135.4324234234 +4823849249230.f +19203234723472-e102 +0000000000000.00000000000000000001 +234324+e4443 \ No newline at end of file diff --git a/contrib/try_convert/data/corr_int2.data b/contrib/try_convert/data/corr_int2.data new file mode 100644 index 00000000000..3d3ada99f23 --- /dev/null +++ b/contrib/try_convert/data/corr_int2.data @@ -0,0 +1,17 @@ +1 +2 +333333333 +333333333333333333333 +5555555555555555555555555555555555555555555 +dfgdg435fw2342rf445 +6.6332342343243242342342342342343243244324 +354345345345435345345345345.23467567867325345345 +$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7 +Twenty-two +12234441231.13123123 +485728435843759345135.4324234234 +4823849249230.f +19203234723472-e102 +0000000000000.00000000000000000001 +234324+e4443 \ No newline at end of file diff --git a/contrib/try_convert/data/corr_int4.data b/contrib/try_convert/data/corr_int4.data new file mode 100644 index 00000000000..3d3ada99f23 --- /dev/null +++ b/contrib/try_convert/data/corr_int4.data @@ -0,0 +1,17 @@ +1 +2 +333333333 +333333333333333333333 +5555555555555555555555555555555555555555555 +dfgdg435fw2342rf445 +6.6332342343243242342342342342343243244324 +354345345345435345345345345.23467567867325345345 +$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7 +Twenty-two +12234441231.13123123 +485728435843759345135.4324234234 +4823849249230.f +19203234723472-e102 +0000000000000.00000000000000000001 +234324+e4443 \ No newline at end of file diff --git a/contrib/try_convert/data/corr_int8.data b/contrib/try_convert/data/corr_int8.data new file mode 100644 index 00000000000..3d3ada99f23 --- /dev/null +++ b/contrib/try_convert/data/corr_int8.data @@ -0,0 +1,17 @@ +1 +2 +333333333 +333333333333333333333 +5555555555555555555555555555555555555555555 +dfgdg435fw2342rf445 +6.6332342343243242342342342342343243244324 +354345345345435345345345345.23467567867325345345 +$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7 +Twenty-two +12234441231.13123123 +485728435843759345135.4324234234 +4823849249230.f +19203234723472-e102 +0000000000000.00000000000000000001 +234324+e4443 \ No newline at end of file diff --git a/contrib/try_convert/data/corr_json.data b/contrib/try_convert/data/corr_json.data new file mode 100644 index 00000000000..e4e284fe311 --- /dev/null +++ b/contrib/try_convert/data/corr_json.data @@ -0,0 +1,18 @@ +{ + +[ +{"world":"CC", "query":"AA", "line":14, "disabled":false, "date":"CAC", "coauthors":"AB"} +{"world":"CCquery":"AA"} +{,,,,,,} +{"world":"CC",,,,, "query":"AA", "line":14, "disabled":false, "date":"CAC", "coauthors":"AB"} +{"world":"CC", "query"::"AA", "line":14, "disabled":false, "date":"CAC", "coauthors":"AB"} + +{"world"} +{"world":"CC", "query":["AA":"BB", "sds"], "line":14, "disabled":false, "date":"CAC", "coauthors":"AB"} +}}}}}}}} +[[[[[[]]]]]] +{[]} +[{}] +][][][][][][] +{}}{}{}}}{{{}{}{}{}}}} +kfgbidsfgadgbdfiugbhdiug \ No newline at end of file diff --git a/contrib/try_convert/data/corr_numeric.data b/contrib/try_convert/data/corr_numeric.data new file mode 100644 index 00000000000..3d3ada99f23 --- /dev/null +++ b/contrib/try_convert/data/corr_numeric.data @@ -0,0 +1,17 @@ +1 +2 +333333333 +333333333333333333333 +5555555555555555555555555555555555555555555 +dfgdg435fw2342rf445 +6.6332342343243242342342342342343243244324 +354345345345435345345345345.23467567867325345345 +$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7 +Twenty-two +12234441231.13123123 +485728435843759345135.4324234234 +4823849249230.f +19203234723472-e102 +0000000000000.00000000000000000001 +234324+e4443 \ No newline at end of file diff --git a/contrib/try_convert/data/corr_time.data b/contrib/try_convert/data/corr_time.data new file mode 100644 index 00000000000..179fdf1c110 --- /dev/null +++ b/contrib/try_convert/data/corr_time.data @@ -0,0 +1,32 @@ +13:35:45 +13:35:90 +13:121:45 +2323:35:45 +13:35:-45 +13:-35:45 +-13:35:45 + +12:52:43 AM +15:52:43 AM +12:152:43 AM +12:52:143 AM +12:52:43 PM +15:52:43 PM +12:152:43 PM +12:52:143 PM + +21:52:07 JST +21:52:07 MSSSSSK + +21:52:07+03:02 +21:52:07+33:02 +21:52:07-33:02 +21:52:07+03:222 + +1982-06-27 18:52:43 +1982-06-32 18:52:43 +1982-32-27 18:52:43 +99999999999999-06-27 18:52:43 + +1982-12-12 01:01:59+09:00 +1982-12-12 01:01:59+19:00 \ No newline at end of file diff --git a/contrib/try_convert/data/corr_timestamp.data b/contrib/try_convert/data/corr_timestamp.data new file mode 100644 index 00000000000..179fdf1c110 --- /dev/null +++ b/contrib/try_convert/data/corr_timestamp.data @@ -0,0 +1,32 @@ +13:35:45 +13:35:90 +13:121:45 +2323:35:45 +13:35:-45 +13:-35:45 +-13:35:45 + +12:52:43 AM +15:52:43 AM +12:152:43 AM +12:52:143 AM +12:52:43 PM +15:52:43 PM +12:152:43 PM +12:52:143 PM + +21:52:07 JST +21:52:07 MSSSSSK + +21:52:07+03:02 +21:52:07+33:02 +21:52:07-33:02 +21:52:07+03:222 + +1982-06-27 18:52:43 +1982-06-32 18:52:43 +1982-32-27 18:52:43 +99999999999999-06-27 18:52:43 + +1982-12-12 01:01:59+09:00 +1982-12-12 01:01:59+19:00 \ No newline at end of file diff --git a/contrib/try_convert/data/corr_timestamptz.data b/contrib/try_convert/data/corr_timestamptz.data new file mode 100644 index 00000000000..179fdf1c110 --- /dev/null +++ b/contrib/try_convert/data/corr_timestamptz.data @@ -0,0 +1,32 @@ +13:35:45 +13:35:90 +13:121:45 +2323:35:45 +13:35:-45 +13:-35:45 +-13:35:45 + +12:52:43 AM +15:52:43 AM +12:152:43 AM +12:52:143 AM +12:52:43 PM +15:52:43 PM +12:152:43 PM +12:52:143 PM + +21:52:07 JST +21:52:07 MSSSSSK + +21:52:07+03:02 +21:52:07+33:02 +21:52:07-33:02 +21:52:07+03:222 + +1982-06-27 18:52:43 +1982-06-32 18:52:43 +1982-32-27 18:52:43 +99999999999999-06-27 18:52:43 + +1982-12-12 01:01:59+09:00 +1982-12-12 01:01:59+19:00 \ No newline at end of file diff --git a/contrib/try_convert/data/corr_timetz.data b/contrib/try_convert/data/corr_timetz.data new file mode 100644 index 00000000000..179fdf1c110 --- /dev/null +++ b/contrib/try_convert/data/corr_timetz.data @@ -0,0 +1,32 @@ +13:35:45 +13:35:90 +13:121:45 +2323:35:45 +13:35:-45 +13:-35:45 +-13:35:45 + +12:52:43 AM +15:52:43 AM +12:152:43 AM +12:52:143 AM +12:52:43 PM +15:52:43 PM +12:152:43 PM +12:52:143 PM + +21:52:07 JST +21:52:07 MSSSSSK + +21:52:07+03:02 +21:52:07+33:02 +21:52:07-33:02 +21:52:07+03:222 + +1982-06-27 18:52:43 +1982-06-32 18:52:43 +1982-32-27 18:52:43 +99999999999999-06-27 18:52:43 + +1982-12-12 01:01:59+09:00 +1982-12-12 01:01:59+19:00 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_abstime.data b/contrib/try_convert/data/tt_abstime.data new file mode 100644 index 00000000000..5cf22efbee9 --- /dev/null +++ b/contrib/try_convert/data/tt_abstime.data @@ -0,0 +1,12 @@ +2015-10-28 16:35:45 +1974-02-15 15:52:07 +2019-07-22 23:19:33 +2024-06-24 23:02:00 +1986-12-03 12:16:50 +1982-12-11 19:01:59 +1979-07-17 08:59:54 +1982-06-27 18:52:43 +2001-06-25 20:23:38 +1975-02-17 19:50:17 +2007-06-14 13:50:09 +1973-06-11 12:06:52 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_bit.data b/contrib/try_convert/data/tt_bit.data new file mode 100644 index 00000000000..f6724da39ab --- /dev/null +++ b/contrib/try_convert/data/tt_bit.data @@ -0,0 +1,11 @@ +1 +0 +1010101 +11001010 +1011011101111 +01001 +1111 +00000000000000000000000 +1111111111111111111 +0101001010101 +11101001000100 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_bool.data b/contrib/try_convert/data/tt_bool.data new file mode 100644 index 00000000000..78542aab4c2 --- /dev/null +++ b/contrib/try_convert/data/tt_bool.data @@ -0,0 +1,19 @@ +f +true +t +false +t +t +f +t +False +t +f +True +TRUE +f +f +1 +0 +f +FALSE \ No newline at end of file diff --git a/contrib/try_convert/data/tt_bpchar.data b/contrib/try_convert/data/tt_bpchar.data new file mode 100644 index 00000000000..21c17124dfc --- /dev/null +++ b/contrib/try_convert/data/tt_bpchar.data @@ -0,0 +1,70 @@ +All the world's a stage, +And all the men and women merely players; +They have their exits and their entrances, +And one man in his time plays many parts, +His acts being seven ages. At first, the infant, +Mewling and puking in the nurse's arms. +Then the whining schoolboy, with his satchel +And shining morning face, creeping like snail +Unwillingly to school. And then the lover, +Sighing like furnace, with a woeful ballad +Made to his mistress' eyebrow. Then a soldier, +Full of strange oaths and bearded like the pard, +Jealous in honor, sudden and quick in quarrel, +Seeking the bubble reputation +Even in the cannon's mouth. And then the justice, +In fair round belly with good capon lined, +With eyes severe and beard of formal cut, +Full of wise saws and modern instances; +And so he plays his part. The sixth age shifts +Into the lean and slippered pantaloon, +With spectacles on nose and pouch on side; +His youthful hose, well saved, a world too wide +For his shrunk shank, and his big manly voice, +Turning again toward childish treble, pipes +And whistles in his sound. Last scene of all, +That ends this strange eventful history, +Is second childishness and mere oblivion, +Sans teeth, sans eyes, sans taste, sans everything. + +std::to_string + C++ Strings library std::basic_string +Defined in header +std::string to_string( int value ); +std::string to_string( long value ); +std::string to_string( long long value ); +std::string to_string( unsigned value ); +std::string to_string( unsigned long value ); +std::string to_string( unsigned long long value ); +std::string to_string( float value ); +std::string to_string( double value ); +std::string to_string( long double value ); +Converts a numeric value to std::string. + +Let buf be an internal to the conversion functions buffer, sufficiently large to contain the result of conversion. + +1) Converts a signed integer to a string as if by std::sprintf(buf, "%d", value). +2) Converts a signed integer to a string as if by std::sprintf(buf, "%ld", value). +3) Converts a signed integer to a string as if by std::sprintf(buf, "%lld", value). +4) Converts an unsigned integer to a string as if by std::sprintf(buf, "%u", value). +5) Converts an unsigned integer to a string as if by std::sprintf(buf, "%lu", value). +6) Converts an unsigned integer to a string as if by std::sprintf(buf, "%llu", value). +7,8) Converts a floating point value to a string as if by std::sprintf(buf, "%f", value). +9) Converts a floating point value to a string as if by std::sprintf(buf, "%Lf", value). +(until C++26) +1-9) Converts a numeric value to a string as if by std::format("{}", value). +(since C++26) +Parameters +Return value +A string holding the converted value. + +Exceptions +May throw std::bad_alloc from the std::string constructor. + +Notes +With floating point types std::to_string may yield unexpected results as the number of significant digits in the returned string can be zero, see the example. +The return value may differ significantly from what std::cout prints by default, see the example. +std::to_string relies on the current C locale for formatting purposes, and therefore concurrent calls to std::to_string from multiple threads may result in partial serialization of calls. +The results of overloads for integer types do not rely on the current C locale, and thus implementations generally avoid access to the current C locale in these overloads for both correctness and performance. However, such avoidance is not guaranteed by the standard. +(until C++26) +C++17 provides std::to_chars as a higher-performance locale-independent alternative. \ No newline at end of file diff --git a/contrib/try_convert/data/tt_char.data b/contrib/try_convert/data/tt_char.data new file mode 100644 index 00000000000..21c17124dfc --- /dev/null +++ b/contrib/try_convert/data/tt_char.data @@ -0,0 +1,70 @@ +All the world's a stage, +And all the men and women merely players; +They have their exits and their entrances, +And one man in his time plays many parts, +His acts being seven ages. At first, the infant, +Mewling and puking in the nurse's arms. +Then the whining schoolboy, with his satchel +And shining morning face, creeping like snail +Unwillingly to school. And then the lover, +Sighing like furnace, with a woeful ballad +Made to his mistress' eyebrow. Then a soldier, +Full of strange oaths and bearded like the pard, +Jealous in honor, sudden and quick in quarrel, +Seeking the bubble reputation +Even in the cannon's mouth. And then the justice, +In fair round belly with good capon lined, +With eyes severe and beard of formal cut, +Full of wise saws and modern instances; +And so he plays his part. The sixth age shifts +Into the lean and slippered pantaloon, +With spectacles on nose and pouch on side; +His youthful hose, well saved, a world too wide +For his shrunk shank, and his big manly voice, +Turning again toward childish treble, pipes +And whistles in his sound. Last scene of all, +That ends this strange eventful history, +Is second childishness and mere oblivion, +Sans teeth, sans eyes, sans taste, sans everything. + +std::to_string + C++ Strings library std::basic_string +Defined in header +std::string to_string( int value ); +std::string to_string( long value ); +std::string to_string( long long value ); +std::string to_string( unsigned value ); +std::string to_string( unsigned long value ); +std::string to_string( unsigned long long value ); +std::string to_string( float value ); +std::string to_string( double value ); +std::string to_string( long double value ); +Converts a numeric value to std::string. + +Let buf be an internal to the conversion functions buffer, sufficiently large to contain the result of conversion. + +1) Converts a signed integer to a string as if by std::sprintf(buf, "%d", value). +2) Converts a signed integer to a string as if by std::sprintf(buf, "%ld", value). +3) Converts a signed integer to a string as if by std::sprintf(buf, "%lld", value). +4) Converts an unsigned integer to a string as if by std::sprintf(buf, "%u", value). +5) Converts an unsigned integer to a string as if by std::sprintf(buf, "%lu", value). +6) Converts an unsigned integer to a string as if by std::sprintf(buf, "%llu", value). +7,8) Converts a floating point value to a string as if by std::sprintf(buf, "%f", value). +9) Converts a floating point value to a string as if by std::sprintf(buf, "%Lf", value). +(until C++26) +1-9) Converts a numeric value to a string as if by std::format("{}", value). +(since C++26) +Parameters +Return value +A string holding the converted value. + +Exceptions +May throw std::bad_alloc from the std::string constructor. + +Notes +With floating point types std::to_string may yield unexpected results as the number of significant digits in the returned string can be zero, see the example. +The return value may differ significantly from what std::cout prints by default, see the example. +std::to_string relies on the current C locale for formatting purposes, and therefore concurrent calls to std::to_string from multiple threads may result in partial serialization of calls. +The results of overloads for integer types do not rely on the current C locale, and thus implementations generally avoid access to the current C locale in these overloads for both correctness and performance. However, such avoidance is not guaranteed by the standard. +(until C++26) +C++17 provides std::to_chars as a higher-performance locale-independent alternative. \ No newline at end of file diff --git a/contrib/try_convert/data/tt_cidr.data b/contrib/try_convert/data/tt_cidr.data new file mode 100644 index 00000000000..73d05ee9bdc --- /dev/null +++ b/contrib/try_convert/data/tt_cidr.data @@ -0,0 +1,16 @@ +192.168.100.128/25 +192.168/24 +192.168/25 +192.168.1 +192.168 +128.1 +128 +128.1.2 +10.1.2 +10.1 +10 +10.1.2.3/32 +2001:4f8:3:ba::/64 +2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128 +::ffff:1.2.3.0/120 +::ffff:1.2.3.0/128 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_citext.data b/contrib/try_convert/data/tt_citext.data new file mode 100644 index 00000000000..21c17124dfc --- /dev/null +++ b/contrib/try_convert/data/tt_citext.data @@ -0,0 +1,70 @@ +All the world's a stage, +And all the men and women merely players; +They have their exits and their entrances, +And one man in his time plays many parts, +His acts being seven ages. At first, the infant, +Mewling and puking in the nurse's arms. +Then the whining schoolboy, with his satchel +And shining morning face, creeping like snail +Unwillingly to school. And then the lover, +Sighing like furnace, with a woeful ballad +Made to his mistress' eyebrow. Then a soldier, +Full of strange oaths and bearded like the pard, +Jealous in honor, sudden and quick in quarrel, +Seeking the bubble reputation +Even in the cannon's mouth. And then the justice, +In fair round belly with good capon lined, +With eyes severe and beard of formal cut, +Full of wise saws and modern instances; +And so he plays his part. The sixth age shifts +Into the lean and slippered pantaloon, +With spectacles on nose and pouch on side; +His youthful hose, well saved, a world too wide +For his shrunk shank, and his big manly voice, +Turning again toward childish treble, pipes +And whistles in his sound. Last scene of all, +That ends this strange eventful history, +Is second childishness and mere oblivion, +Sans teeth, sans eyes, sans taste, sans everything. + +std::to_string + C++ Strings library std::basic_string +Defined in header +std::string to_string( int value ); +std::string to_string( long value ); +std::string to_string( long long value ); +std::string to_string( unsigned value ); +std::string to_string( unsigned long value ); +std::string to_string( unsigned long long value ); +std::string to_string( float value ); +std::string to_string( double value ); +std::string to_string( long double value ); +Converts a numeric value to std::string. + +Let buf be an internal to the conversion functions buffer, sufficiently large to contain the result of conversion. + +1) Converts a signed integer to a string as if by std::sprintf(buf, "%d", value). +2) Converts a signed integer to a string as if by std::sprintf(buf, "%ld", value). +3) Converts a signed integer to a string as if by std::sprintf(buf, "%lld", value). +4) Converts an unsigned integer to a string as if by std::sprintf(buf, "%u", value). +5) Converts an unsigned integer to a string as if by std::sprintf(buf, "%lu", value). +6) Converts an unsigned integer to a string as if by std::sprintf(buf, "%llu", value). +7,8) Converts a floating point value to a string as if by std::sprintf(buf, "%f", value). +9) Converts a floating point value to a string as if by std::sprintf(buf, "%Lf", value). +(until C++26) +1-9) Converts a numeric value to a string as if by std::format("{}", value). +(since C++26) +Parameters +Return value +A string holding the converted value. + +Exceptions +May throw std::bad_alloc from the std::string constructor. + +Notes +With floating point types std::to_string may yield unexpected results as the number of significant digits in the returned string can be zero, see the example. +The return value may differ significantly from what std::cout prints by default, see the example. +std::to_string relies on the current C locale for formatting purposes, and therefore concurrent calls to std::to_string from multiple threads may result in partial serialization of calls. +The results of overloads for integer types do not rely on the current C locale, and thus implementations generally avoid access to the current C locale in these overloads for both correctness and performance. However, such avoidance is not guaranteed by the standard. +(until C++26) +C++17 provides std::to_chars as a higher-performance locale-independent alternative. \ No newline at end of file diff --git a/contrib/try_convert/data/tt_complex.data b/contrib/try_convert/data/tt_complex.data new file mode 100644 index 00000000000..d7ebf7eb244 --- /dev/null +++ b/contrib/try_convert/data/tt_complex.data @@ -0,0 +1,12 @@ +1.1 + 1.0i +10.0 +-1213 - 13.342i +1546i +0.00004 + 9i +111111 - 0.00i +0i +0 +-111.234 +567.1 +123331 +-2 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_date.data b/contrib/try_convert/data/tt_date.data new file mode 100644 index 00000000000..a933b76310e --- /dev/null +++ b/contrib/try_convert/data/tt_date.data @@ -0,0 +1,12 @@ +2015-10-28 +1974-02-15 +2019-07-22 +2024-06-24 +1986-12-03 +1982-12-12 +1979-07-17 +1982-06-27 +2001-06-25 +1975-02-18 +2007-06-14 +1973-06-11 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_float4.data b/contrib/try_convert/data/tt_float4.data new file mode 100644 index 00000000000..d634a540948 --- /dev/null +++ b/contrib/try_convert/data/tt_float4.data @@ -0,0 +1,24 @@ +5.09526 +0.90909 +0.47116 +1.09649 +62.7446 +79.2079 +42.2159 +6.35277 +381.619 +996.121 +529.114 +971.078 +8607.79 +114.810 +7207.21 +6817.10 +53697.0 +26682.5 +64096.1 +11155.2 +434765 +453723 +953815 +875852 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_float8.data b/contrib/try_convert/data/tt_float8.data new file mode 100644 index 00000000000..441d46d8a57 --- /dev/null +++ b/contrib/try_convert/data/tt_float8.data @@ -0,0 +1,30 @@ +2.6338905075109076 +5.005861130502983 +17.865188053013135 +91.26278393448204 +870.5185698367669 +298.4447914486329 +6389.494948660052 +6089.702114381723 +15283.926854963482 +76251.08000751512 +539379.0301196257 +778626.4786305582 +5303536.721951775 +5718.961279435053 +32415605.70046731 +1947674.2385832302 +929098616.2646171 +878721877.8231843 +8316655293.611794 +3075141254.026614 +5792516649.418756 +87800959920.40405 +946949445297.994 +85653452067.87878 +4859904633166.138 +692125184683.836 +76060216525723.16 +76583442930698.78 +128391464499762.8 +475282378098731.3 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_hstore.data b/contrib/try_convert/data/tt_hstore.data new file mode 100644 index 00000000000..6d21a65218e --- /dev/null +++ b/contrib/try_convert/data/tt_hstore.data @@ -0,0 +1,12 @@ +k => v +foo => bar, baz => whatever +"1-a" => "anything at all" +k => v +foo => bar, baz => whatever +"1-a" => "anything at all" +k => v +foo => bar, baz => whatever +"1-a" => "anything at all" +k => v +foo => bar, baz => whatever +"1-a" => "anything at all" \ No newline at end of file diff --git a/contrib/try_convert/data/tt_inet.data b/contrib/try_convert/data/tt_inet.data new file mode 100644 index 00000000000..e5231166925 --- /dev/null +++ b/contrib/try_convert/data/tt_inet.data @@ -0,0 +1,19 @@ +192.168.100.128/25 +192.168.0.0/24 +192.168.0.0/25 +192.168.100.128 +192.168.0.1/24 +192.168.0.1/25 +192.168.1.0 +192.168.0.0 +128.1.0.0 +128.0.0.0 +128.1.2.0 +10.1.2.0 +10.1.0.0 +10.0.0.0 +10.1.2.3/32 +2001:4f8:3:ba::/64 +2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128 +::ffff:1.2.3.0/120 +::ffff:1.2.3.0/128 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_int2.data b/contrib/try_convert/data/tt_int2.data new file mode 100644 index 00000000000..d6021f2c5e3 --- /dev/null +++ b/contrib/try_convert/data/tt_int2.data @@ -0,0 +1,24 @@ +6 +0 +2 +2 +7 +6 +89 +8 +42 +2 +21 +50 +26 +198 +649 +544 +220 +589 +8094 +64 +8058 +6981 +3402 +1554 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_int4.data b/contrib/try_convert/data/tt_int4.data new file mode 100644 index 00000000000..1508f6a912a --- /dev/null +++ b/contrib/try_convert/data/tt_int4.data @@ -0,0 +1,27 @@ +9 +3 +0 +9 +84 +60 +807 +729 +536 +9731 +3785 +5520 +82940 +61851 +86170 +577352 +704571 +45824 +2278982 +2893879 +797919 +23279088 +10100142 +27797360 +635684444 +364832178 +370180967 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_int8.data b/contrib/try_convert/data/tt_int8.data new file mode 100644 index 00000000000..cb2afdd7cb2 --- /dev/null +++ b/contrib/try_convert/data/tt_int8.data @@ -0,0 +1,36 @@ +2 +2 +93 +64 +609 +171 +7291 +1634 +37945 +98952 +639999 +556949 +6846142 +8428519 +77599991 +22904807 +32100243 +315453048 +2677408759 +2109828435 +94290971433 +87636762647 +314677880798 +655438665294 +3956319010606 +9145475897405 +45885185258739 +26488016649805 +246627507693983 +561368134163150 +2627416085229352 +5845859902235405 +89782288360247696 +39940050514039728 +219320759157283328 +997537606495110272 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_interval.data b/contrib/try_convert/data/tt_interval.data new file mode 100644 index 00000000000..3f44dbbc925 --- /dev/null +++ b/contrib/try_convert/data/tt_interval.data @@ -0,0 +1,12 @@ +16736 days, 13:35:45 +1506 days, 12:52:07 +18099 days, 20:19:33 +19898 days, 20:02:00 +6180 days, 9:16:50 +4727 days, 16:01:59 +3484 days, 5:59:54 +4560 days, 15:52:43 +11498 days, 17:23:38 +1873 days, 16:50:17 +13678 days, 10:50:09 +1257 days, 9:06:52 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_json.data b/contrib/try_convert/data/tt_json.data new file mode 100644 index 00000000000..947eac50831 --- /dev/null +++ b/contrib/try_convert/data/tt_json.data @@ -0,0 +1,29 @@ +{"glossary": {"title": "example glossary","GlossDiv": {"title": "S","GlossList": {"GlossEntry": {"ID": "SGML","SortAs": "SGML","GlossTerm": "Standard Generalized Markup Language","Acronym": "SGML","Abbrev": "ISO 8879:1986","GlossDef": {"para": "A meta-markup language, used to create markup languages such as DocBook.","GlossSeeAlso": ["GML", "XML"]},"GlossSee": "markup"}}}}} +{"menu": {"id": "file","value": "File","popup": {"menuitem": [{"value": "New", "onclick": "CreateNewDoc()"},{"value": "Open", "onclick": "OpenDoc()"},{"value": "Close", "onclick": "CloseDoc()"}]}}} +{"widget": {"debug": "on","window": {"title": "Sample Konfabulator Widget","name": "main_window","width": 500,"height": 500},"image": { "src": "Images/Sun.png","name": "sun1","hOffset": 250,"vOffset": 250,"alignment": "center"},"text": {"data": "Click Here","size": 36,"style": "bold","name": "text1","hOffset": 250,"vOffset": 100,"alignment": "center","onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"}}} +{"menu": {"header": "SVG Viewer","items": [{"id": "Open"},{"id": "OpenNew", "label": "Open New"},null,{"id": "ZoomIn", "label": "Zoom In"},{"id": "ZoomOut", "label": "Zoom Out"},{"id": "OriginalView", "label": "Original View"},null,{"id": "Quality"},{"id": "Pause"},{"id": "Mute"},null,{"id": "Find", "label": "Find..."},{"id": "FindAgain", "label": "Find Again"},{"id": "Copy"},{"id": "CopyAgain", "label": "Copy Again"},{"id": "CopySVG", "label": "Copy SVG"},{"id": "ViewSVG", "label": "View SVG"},{"id": "ViewSource", "label": "View Source"},{"id": "SaveAs", "label": "Save As"},null,{"id": "Help"},{"id": "About", "label": "About Adobe CVG Viewer..."}]}} +{"line":1, "date":"CB", "node":"AA"} +{"cleaned":false, "status":59, "line":2, "disabled":false, "node":"CBB"} +{"indexed":true, "status":35, "line":3, "disabled":false, "wait":"CAA", "subtitle":"BA", "user":"CCA"} +{"line":4, "disabled":true, "space":"BB"} +{"cleaned":false, "line":5, "wait":"BB", "query":"CAC", "coauthors":"ACA", "node":"CBA"} +{"world":"CB", "query":"CBC", "indexed":false, "line":6, "pos":92, "date":"AAB", "space":"CB", "coauthors":"ACA", "node":"CBC"} +{"state":98, "org":43, "line":7, "pos":97} +{"auth":"BB", "title":"CAC", "query":"BA", "status":94, "line":8, "coauthors":"BBB"} +{"auth":"BAC", "title":"CAA", "wait":"CA", "bad":true, "query":"AA", "indexed":true, "line":9, "pos":56} +{"title":"AAC", "bad":true, "user":"AAB", "query":"AC", "line":10, "node":"AB"} +{"world":"CAC", "user":"AB", "query":"ACA", "indexed":true, "line":11, "space":"CB"} +{"line":12, "pos":72, "abstract":"BBA", "space":"AAC"} +{} +{"world":"CC", "query":"AA", "line":14, "disabled":false, "date":"CAC", "coauthors":"AB"} +{"org":68, "title":"BBB", "query":"BAC", "line":15, "public":false} +{"org":73, "user":"AA", "indexed":true, "line":16, "date":"CCC", "public":true, "coauthors":"AB"} +{"indexed":false, "line":17} +{"state":23, "auth":"BCC", "org":38, "status":28, "line":18, "disabled":false, "abstract":"CB"} +{"state":99, "auth":"CA", "indexed":true, "line":19, "date":"BA"} +{"wait":"CBA", "user":"BBA", "indexed":true, "line":20, "disabled":false, "abstract":"BA", "date":"ABA"} +{"org":10, "query":"AC", "indexed":false, "line":21, "disabled":true, "abstract":"CA", "pos":44} +{"state":65, "title":"AC", "user":"AAC", "cleaned":true, "status":93, "line":22, "abstract":"ABC", "node":"CCC"} +{"subtitle":"AC", "user":"CCC", "line":23} +{"state":67, "world":"ACB", "bad":true, "user":"CB", "line":24, "disabled":true} +{} \ No newline at end of file diff --git a/contrib/try_convert/data/tt_jsonb.data b/contrib/try_convert/data/tt_jsonb.data new file mode 100644 index 00000000000..947eac50831 --- /dev/null +++ b/contrib/try_convert/data/tt_jsonb.data @@ -0,0 +1,29 @@ +{"glossary": {"title": "example glossary","GlossDiv": {"title": "S","GlossList": {"GlossEntry": {"ID": "SGML","SortAs": "SGML","GlossTerm": "Standard Generalized Markup Language","Acronym": "SGML","Abbrev": "ISO 8879:1986","GlossDef": {"para": "A meta-markup language, used to create markup languages such as DocBook.","GlossSeeAlso": ["GML", "XML"]},"GlossSee": "markup"}}}}} +{"menu": {"id": "file","value": "File","popup": {"menuitem": [{"value": "New", "onclick": "CreateNewDoc()"},{"value": "Open", "onclick": "OpenDoc()"},{"value": "Close", "onclick": "CloseDoc()"}]}}} +{"widget": {"debug": "on","window": {"title": "Sample Konfabulator Widget","name": "main_window","width": 500,"height": 500},"image": { "src": "Images/Sun.png","name": "sun1","hOffset": 250,"vOffset": 250,"alignment": "center"},"text": {"data": "Click Here","size": 36,"style": "bold","name": "text1","hOffset": 250,"vOffset": 100,"alignment": "center","onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"}}} +{"menu": {"header": "SVG Viewer","items": [{"id": "Open"},{"id": "OpenNew", "label": "Open New"},null,{"id": "ZoomIn", "label": "Zoom In"},{"id": "ZoomOut", "label": "Zoom Out"},{"id": "OriginalView", "label": "Original View"},null,{"id": "Quality"},{"id": "Pause"},{"id": "Mute"},null,{"id": "Find", "label": "Find..."},{"id": "FindAgain", "label": "Find Again"},{"id": "Copy"},{"id": "CopyAgain", "label": "Copy Again"},{"id": "CopySVG", "label": "Copy SVG"},{"id": "ViewSVG", "label": "View SVG"},{"id": "ViewSource", "label": "View Source"},{"id": "SaveAs", "label": "Save As"},null,{"id": "Help"},{"id": "About", "label": "About Adobe CVG Viewer..."}]}} +{"line":1, "date":"CB", "node":"AA"} +{"cleaned":false, "status":59, "line":2, "disabled":false, "node":"CBB"} +{"indexed":true, "status":35, "line":3, "disabled":false, "wait":"CAA", "subtitle":"BA", "user":"CCA"} +{"line":4, "disabled":true, "space":"BB"} +{"cleaned":false, "line":5, "wait":"BB", "query":"CAC", "coauthors":"ACA", "node":"CBA"} +{"world":"CB", "query":"CBC", "indexed":false, "line":6, "pos":92, "date":"AAB", "space":"CB", "coauthors":"ACA", "node":"CBC"} +{"state":98, "org":43, "line":7, "pos":97} +{"auth":"BB", "title":"CAC", "query":"BA", "status":94, "line":8, "coauthors":"BBB"} +{"auth":"BAC", "title":"CAA", "wait":"CA", "bad":true, "query":"AA", "indexed":true, "line":9, "pos":56} +{"title":"AAC", "bad":true, "user":"AAB", "query":"AC", "line":10, "node":"AB"} +{"world":"CAC", "user":"AB", "query":"ACA", "indexed":true, "line":11, "space":"CB"} +{"line":12, "pos":72, "abstract":"BBA", "space":"AAC"} +{} +{"world":"CC", "query":"AA", "line":14, "disabled":false, "date":"CAC", "coauthors":"AB"} +{"org":68, "title":"BBB", "query":"BAC", "line":15, "public":false} +{"org":73, "user":"AA", "indexed":true, "line":16, "date":"CCC", "public":true, "coauthors":"AB"} +{"indexed":false, "line":17} +{"state":23, "auth":"BCC", "org":38, "status":28, "line":18, "disabled":false, "abstract":"CB"} +{"state":99, "auth":"CA", "indexed":true, "line":19, "date":"BA"} +{"wait":"CBA", "user":"BBA", "indexed":true, "line":20, "disabled":false, "abstract":"BA", "date":"ABA"} +{"org":10, "query":"AC", "indexed":false, "line":21, "disabled":true, "abstract":"CA", "pos":44} +{"state":65, "title":"AC", "user":"AAC", "cleaned":true, "status":93, "line":22, "abstract":"ABC", "node":"CCC"} +{"subtitle":"AC", "user":"CCC", "line":23} +{"state":67, "world":"ACB", "bad":true, "user":"CB", "line":24, "disabled":true} +{} \ No newline at end of file diff --git a/contrib/try_convert/data/tt_macaddr.data b/contrib/try_convert/data/tt_macaddr.data new file mode 100644 index 00000000000..4a05521feab --- /dev/null +++ b/contrib/try_convert/data/tt_macaddr.data @@ -0,0 +1,12 @@ +08:00:2b:01:02:03 +08-00-2b-01-02-03 +08002b:010203 +08002b-010203 +0800.2b01.0203 +08002b010203 +08:00:2b:01:02:03 +08-00-2b-01-02-03 +08002b:010203 +08002b-010203 +0800.2b01.0203 +08002b010203 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_money.data b/contrib/try_convert/data/tt_money.data new file mode 100644 index 00000000000..3ec7528e04c --- /dev/null +++ b/contrib/try_convert/data/tt_money.data @@ -0,0 +1,16 @@ +10 +0.01 +10.03 +$9 +$1,000,000,000,000,000.00 +0 +$555555555 +1,656,343 +10 +0.01 +10.03 +$9 +$1,000,000,000,000,000.00 +0 +$555555555 +1,656,343 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_numeric.data b/contrib/try_convert/data/tt_numeric.data new file mode 100644 index 00000000000..0651147684a --- /dev/null +++ b/contrib/try_convert/data/tt_numeric.data @@ -0,0 +1,30 @@ +5.49803593494943 +2.65056628940059 +87.2433041085257 +42.3137940200886 +211.798205442082 +539.296088779458 +7299.31069089976 +2011.51063389695 +31171.6291300894 +99514.9356660894 +649878.057639453 +438100.083914504 +5175758.4103559 +1210041.95868265 +22469733.7031557 +33808556.2147455 +588308718.457233 +230114732.596577 +2202173844.51559 +709930860.090325 +63110295727.0098 +22894178381.1154 +905420013006.127 +859635400253.746 +708573498886.534 +2380046343689.95 +66897777829628.1 +21423680737043.2 +132311848725025 +935514240580671 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_oid.data b/contrib/try_convert/data/tt_oid.data new file mode 100644 index 00000000000..1d1d3048863 --- /dev/null +++ b/contrib/try_convert/data/tt_oid.data @@ -0,0 +1,13 @@ +232 +2908 +1069 +20 +21 +24 +2950 +701 +18 +19 +114 +1247 +1259 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_point.data b/contrib/try_convert/data/tt_point.data new file mode 100644 index 00000000000..611184758d0 --- /dev/null +++ b/contrib/try_convert/data/tt_point.data @@ -0,0 +1,16 @@ +(1, 2) +(-100000, 0) +21, 9999999999 +0.011111, 9 +962856498.3423, -243.24 +3321, 123 +23,12 +1321 , 132 +216 ,345354 +( 21, 9999999999 ) +( 0.011111, 9 ) +( 962856498.3423, -243.24 ) +(3321, 123 ) +( 23,12) +(1321 , 132 ) +( 216 ,345354) \ No newline at end of file diff --git a/contrib/try_convert/data/tt_regclass.data b/contrib/try_convert/data/tt_regclass.data new file mode 100644 index 00000000000..df7234772a6 --- /dev/null +++ b/contrib/try_convert/data/tt_regclass.data @@ -0,0 +1,12 @@ +pg_type +pg_proc +pg_class +pg_attribute +pg_user +pg_statistic +pg_class_oid_index +pg_views +pg_timezone_names +pg_stat_database +pg_tables +pg_roles \ No newline at end of file diff --git a/contrib/try_convert/data/tt_regproc.data b/contrib/try_convert/data/tt_regproc.data new file mode 100644 index 00000000000..c1fdfdc54fd --- /dev/null +++ b/contrib/try_convert/data/tt_regproc.data @@ -0,0 +1,11 @@ +textin +int4lt +float8abs +232 +2908 +now +pg_stat_get_activity +pg_table_size +1069 +inet_client_addr +inet_server_addr \ No newline at end of file diff --git a/contrib/try_convert/data/tt_regtype.data b/contrib/try_convert/data/tt_regtype.data new file mode 100644 index 00000000000..187bba10479 --- /dev/null +++ b/contrib/try_convert/data/tt_regtype.data @@ -0,0 +1,30 @@ +bool + bytea + char + name + int8 + int2 + int2vector + int4 + regproc + text + oid + tid + xid + cid + oidvector + pg_type + pg_attribute + pg_proc + pg_class + json + xml + _xml + pg_node_tree + _json + complex + _complex + smgr + point + lseg + path \ No newline at end of file diff --git a/contrib/try_convert/data/tt_reltime.data b/contrib/try_convert/data/tt_reltime.data new file mode 100644 index 00000000000..3f44dbbc925 --- /dev/null +++ b/contrib/try_convert/data/tt_reltime.data @@ -0,0 +1,12 @@ +16736 days, 13:35:45 +1506 days, 12:52:07 +18099 days, 20:19:33 +19898 days, 20:02:00 +6180 days, 9:16:50 +4727 days, 16:01:59 +3484 days, 5:59:54 +4560 days, 15:52:43 +11498 days, 17:23:38 +1873 days, 16:50:17 +13678 days, 10:50:09 +1257 days, 9:06:52 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_text.data b/contrib/try_convert/data/tt_text.data new file mode 100644 index 00000000000..21c17124dfc --- /dev/null +++ b/contrib/try_convert/data/tt_text.data @@ -0,0 +1,70 @@ +All the world's a stage, +And all the men and women merely players; +They have their exits and their entrances, +And one man in his time plays many parts, +His acts being seven ages. At first, the infant, +Mewling and puking in the nurse's arms. +Then the whining schoolboy, with his satchel +And shining morning face, creeping like snail +Unwillingly to school. And then the lover, +Sighing like furnace, with a woeful ballad +Made to his mistress' eyebrow. Then a soldier, +Full of strange oaths and bearded like the pard, +Jealous in honor, sudden and quick in quarrel, +Seeking the bubble reputation +Even in the cannon's mouth. And then the justice, +In fair round belly with good capon lined, +With eyes severe and beard of formal cut, +Full of wise saws and modern instances; +And so he plays his part. The sixth age shifts +Into the lean and slippered pantaloon, +With spectacles on nose and pouch on side; +His youthful hose, well saved, a world too wide +For his shrunk shank, and his big manly voice, +Turning again toward childish treble, pipes +And whistles in his sound. Last scene of all, +That ends this strange eventful history, +Is second childishness and mere oblivion, +Sans teeth, sans eyes, sans taste, sans everything. + +std::to_string + C++ Strings library std::basic_string +Defined in header +std::string to_string( int value ); +std::string to_string( long value ); +std::string to_string( long long value ); +std::string to_string( unsigned value ); +std::string to_string( unsigned long value ); +std::string to_string( unsigned long long value ); +std::string to_string( float value ); +std::string to_string( double value ); +std::string to_string( long double value ); +Converts a numeric value to std::string. + +Let buf be an internal to the conversion functions buffer, sufficiently large to contain the result of conversion. + +1) Converts a signed integer to a string as if by std::sprintf(buf, "%d", value). +2) Converts a signed integer to a string as if by std::sprintf(buf, "%ld", value). +3) Converts a signed integer to a string as if by std::sprintf(buf, "%lld", value). +4) Converts an unsigned integer to a string as if by std::sprintf(buf, "%u", value). +5) Converts an unsigned integer to a string as if by std::sprintf(buf, "%lu", value). +6) Converts an unsigned integer to a string as if by std::sprintf(buf, "%llu", value). +7,8) Converts a floating point value to a string as if by std::sprintf(buf, "%f", value). +9) Converts a floating point value to a string as if by std::sprintf(buf, "%Lf", value). +(until C++26) +1-9) Converts a numeric value to a string as if by std::format("{}", value). +(since C++26) +Parameters +Return value +A string holding the converted value. + +Exceptions +May throw std::bad_alloc from the std::string constructor. + +Notes +With floating point types std::to_string may yield unexpected results as the number of significant digits in the returned string can be zero, see the example. +The return value may differ significantly from what std::cout prints by default, see the example. +std::to_string relies on the current C locale for formatting purposes, and therefore concurrent calls to std::to_string from multiple threads may result in partial serialization of calls. +The results of overloads for integer types do not rely on the current C locale, and thus implementations generally avoid access to the current C locale in these overloads for both correctness and performance. However, such avoidance is not guaranteed by the standard. +(until C++26) +C++17 provides std::to_chars as a higher-performance locale-independent alternative. \ No newline at end of file diff --git a/contrib/try_convert/data/tt_time.data b/contrib/try_convert/data/tt_time.data new file mode 100644 index 00000000000..92ba52ea639 --- /dev/null +++ b/contrib/try_convert/data/tt_time.data @@ -0,0 +1,12 @@ +13:35:45 +21:52:07 +16:19:33 +20:02:00 +11:16:50 +01:01:59 +14:59:54 +10:52:43 +19:23:38 +01:50:17 +09:50:09 +09:06:52 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_timestamp.data b/contrib/try_convert/data/tt_timestamp.data new file mode 100644 index 00000000000..5cf22efbee9 --- /dev/null +++ b/contrib/try_convert/data/tt_timestamp.data @@ -0,0 +1,12 @@ +2015-10-28 16:35:45 +1974-02-15 15:52:07 +2019-07-22 23:19:33 +2024-06-24 23:02:00 +1986-12-03 12:16:50 +1982-12-11 19:01:59 +1979-07-17 08:59:54 +1982-06-27 18:52:43 +2001-06-25 20:23:38 +1975-02-17 19:50:17 +2007-06-14 13:50:09 +1973-06-11 12:06:52 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_timestamptz.data b/contrib/try_convert/data/tt_timestamptz.data new file mode 100644 index 00000000000..bdb863b79f7 --- /dev/null +++ b/contrib/try_convert/data/tt_timestamptz.data @@ -0,0 +1,12 @@ +2015-10-28 13:35:45+00:00 +1974-02-15 21:52:07+09:00 +2019-07-22 16:19:33-04:00 +2024-06-24 20:02:00+00:00 +1986-12-03 11:16:50+02:00 +1982-12-12 01:01:59+09:00 +1979-07-17 14:59:54+09:00 +1982-06-27 10:52:43-04:00 +2001-06-25 19:23:38+03:00 +1975-02-18 01:50:17+09:00 +2007-06-14 09:50:09+00:00 +1973-06-11 09:06:52+00:00 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_timetz.data b/contrib/try_convert/data/tt_timetz.data new file mode 100644 index 00000000000..cee4c87ffb3 --- /dev/null +++ b/contrib/try_convert/data/tt_timetz.data @@ -0,0 +1,12 @@ +13:35:45 UTC +21:52:07 JST +16:19:33 EDT +20:02:00 UTC +11:16:50 EET +01:01:59 JST +14:59:54 JST +10:52:43 EDT +19:23:38 EEST +01:50:17 JST +09:50:09 UTC +09:06:52 UTC \ No newline at end of file diff --git a/contrib/try_convert/data/tt_uuid.data b/contrib/try_convert/data/tt_uuid.data new file mode 100644 index 00000000000..1ed8f566e0f --- /dev/null +++ b/contrib/try_convert/data/tt_uuid.data @@ -0,0 +1,12 @@ +a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 +A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11 +{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11} +a0eebc999c0b4ef8bb6d6bb9bd380a11 +a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11 +{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11} +a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 +A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11 +{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11} +a0eebc999c0b4ef8bb6d6bb9bd380a11 +a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11 +{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11} \ No newline at end of file diff --git a/contrib/try_convert/data/tt_varbit.data b/contrib/try_convert/data/tt_varbit.data new file mode 100644 index 00000000000..f6724da39ab --- /dev/null +++ b/contrib/try_convert/data/tt_varbit.data @@ -0,0 +1,11 @@ +1 +0 +1010101 +11001010 +1011011101111 +01001 +1111 +00000000000000000000000 +1111111111111111111 +0101001010101 +11101001000100 \ No newline at end of file diff --git a/contrib/try_convert/data/tt_varchar.data b/contrib/try_convert/data/tt_varchar.data new file mode 100644 index 00000000000..21c17124dfc --- /dev/null +++ b/contrib/try_convert/data/tt_varchar.data @@ -0,0 +1,70 @@ +All the world's a stage, +And all the men and women merely players; +They have their exits and their entrances, +And one man in his time plays many parts, +His acts being seven ages. At first, the infant, +Mewling and puking in the nurse's arms. +Then the whining schoolboy, with his satchel +And shining morning face, creeping like snail +Unwillingly to school. And then the lover, +Sighing like furnace, with a woeful ballad +Made to his mistress' eyebrow. Then a soldier, +Full of strange oaths and bearded like the pard, +Jealous in honor, sudden and quick in quarrel, +Seeking the bubble reputation +Even in the cannon's mouth. And then the justice, +In fair round belly with good capon lined, +With eyes severe and beard of formal cut, +Full of wise saws and modern instances; +And so he plays his part. The sixth age shifts +Into the lean and slippered pantaloon, +With spectacles on nose and pouch on side; +His youthful hose, well saved, a world too wide +For his shrunk shank, and his big manly voice, +Turning again toward childish treble, pipes +And whistles in his sound. Last scene of all, +That ends this strange eventful history, +Is second childishness and mere oblivion, +Sans teeth, sans eyes, sans taste, sans everything. + +std::to_string + C++ Strings library std::basic_string +Defined in header +std::string to_string( int value ); +std::string to_string( long value ); +std::string to_string( long long value ); +std::string to_string( unsigned value ); +std::string to_string( unsigned long value ); +std::string to_string( unsigned long long value ); +std::string to_string( float value ); +std::string to_string( double value ); +std::string to_string( long double value ); +Converts a numeric value to std::string. + +Let buf be an internal to the conversion functions buffer, sufficiently large to contain the result of conversion. + +1) Converts a signed integer to a string as if by std::sprintf(buf, "%d", value). +2) Converts a signed integer to a string as if by std::sprintf(buf, "%ld", value). +3) Converts a signed integer to a string as if by std::sprintf(buf, "%lld", value). +4) Converts an unsigned integer to a string as if by std::sprintf(buf, "%u", value). +5) Converts an unsigned integer to a string as if by std::sprintf(buf, "%lu", value). +6) Converts an unsigned integer to a string as if by std::sprintf(buf, "%llu", value). +7,8) Converts a floating point value to a string as if by std::sprintf(buf, "%f", value). +9) Converts a floating point value to a string as if by std::sprintf(buf, "%Lf", value). +(until C++26) +1-9) Converts a numeric value to a string as if by std::format("{}", value). +(since C++26) +Parameters +Return value +A string holding the converted value. + +Exceptions +May throw std::bad_alloc from the std::string constructor. + +Notes +With floating point types std::to_string may yield unexpected results as the number of significant digits in the returned string can be zero, see the example. +The return value may differ significantly from what std::cout prints by default, see the example. +std::to_string relies on the current C locale for formatting purposes, and therefore concurrent calls to std::to_string from multiple threads may result in partial serialization of calls. +The results of overloads for integer types do not rely on the current C locale, and thus implementations generally avoid access to the current C locale in these overloads for both correctness and performance. However, such avoidance is not guaranteed by the standard. +(until C++26) +C++17 provides std::to_chars as a higher-performance locale-independent alternative. \ No newline at end of file diff --git a/contrib/try_convert/data/tt_xml.data b/contrib/try_convert/data/tt_xml.data new file mode 100644 index 00000000000..389588a6525 --- /dev/null +++ b/contrib/try_convert/data/tt_xml.data @@ -0,0 +1,10 @@ + + on main_window 500 500 250 250 center text1 250 100 center sun1.opacity = (sun1.opacity / 100) * 90; + A Song of Ice and Fire George R. R. Martin English Epic fantasy + Rick Grimes 35 Maths Male Daryl Dixon 33 Science Male Maggie 36 Arts Female +
Adobe SVG Viewer
Open Open New Zoom In Zoom Out Original View Quality Pause Mute Find... Find Again Copy Copy Again Copy SVG View SVG View Source Save As Help About Adobe CVG Viewer...
+ + on main_window 500 500 250 250 center text1 250 100 center sun1.opacity = (sun1.opacity / 100) * 90; + A Song of Ice and Fire George R. R. Martin English Epic fantasy + Rick Grimes 35 Maths Male Daryl Dixon 33 Science Male Maggie 36 Arts Female +
Adobe SVG Viewer
Open Open New Zoom In Zoom Out Original View Quality Pause Mute Find... Find Again Copy Copy Again Copy SVG View SVG View Source Save As Help About Adobe CVG Viewer...
\ No newline at end of file diff --git a/contrib/try_convert/scripts/check_test.py b/contrib/try_convert/scripts/check_test.py new file mode 100644 index 00000000000..52e66ed158e --- /dev/null +++ b/contrib/try_convert/scripts/check_test.py @@ -0,0 +1,87 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import re + +regression_path = './regression.diffs' + +f = open(regression_path) +lines = f.read().split('\n') + +needed_types = [ + 'bool', + + 'int2', + 'int4', + 'int8', + 'float4', + 'float8', + 'numeric', + + 'date', + 'time', + 'timestamp', + 'timetz', + 'timestamptz', + 'interval' + + 'regproc', + 'pg_catalog', + 'regclass', + 'regtype', + + 'value_day', + 'oid', + 'jsonb', + 'json', + + # 'text', + # 'bpchar', + # 'varchar', + # 'char' +] + +failed_tests = {} +c = 0 + +for i in range(1, len(lines)): + preline = lines[i-1] + line = lines[i] + if len(line) > 0 and len(preline) > 0 and (line[0] == '-' or line[0] == '+') and (preline[0] != '-' and preline[0] != '+'): + words = re.split('::|\*|;|\n| |\(|\)|,|\.|\".*\"|\'.*\'|<.*>', preline) + ans = [] + is_prining = False + for word in words: + if word not in ['select', 'from', 'count', + 'try_convert', 'try_convert_by_sql', 'try_convert_by_sql_text', 'try_convert_by_sql_with_len_out', + 'NULL', 'v', 'v1', 'v2', 'where', 'is', 'not', 'distinct', 'as', 't', '']: + ans += [word] + for w in word.split('_'): + if w in needed_types: + is_prining = True + if is_prining: + failed_tests[' '.join(ans)] = True + # print(ans, line) + c += 1 + +for ft in failed_tests: + print(ft) + +print(f'Summary: {c}') + \ No newline at end of file diff --git a/contrib/try_convert/scripts/error_safe_check.py b/contrib/try_convert/scripts/error_safe_check.py new file mode 100644 index 00000000000..2852f5eff3e --- /dev/null +++ b/contrib/try_convert/scripts/error_safe_check.py @@ -0,0 +1,143 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import re + +import os, glob + +p_any = '(?:\S| |\t|\n+\t|\n+ |\n+#|\n+/|\n+\$)+?' +p_spaces = '\s*' + +def create_pattern(funcCall): + return '\n(\w+)\s+?(\w+)' + p_spaces + '\((' + p_any + ')\)' + p_spaces + '(\n\{' + '(' + p_any + ')' + f'({funcCall})' + '\s*' + '(' + p_any + ')' + '\n+\})' + +pattern = create_pattern('ereturn') + + +source_filenames = ['time.c', 'date.c', 'timedate.c', 'int.c', 'float.c', 'bool.c', 'char.c', 'formatting.c', 'json.c', 'jsonb.c', 'nabstime.c', 'numeric.c', 'timestamp.c', 'network.c'] + + +# verify all convert and in&outs(if in converts) are 'soft'-error handling +# safe error handle == sub-calls are not 'ereport' (replaced by 'ereturn') + + +boolin = ''' + const char *in_str = PG_GETARG_CSTRING(0); + const char *str; + size_t len; + bool result; + + /* + * Skip leading and trailing whitespace + */ + str = in_str; + while (isspace((unsigned char) *str)) + str++; + + len = strlen(str); + while (len > 0 && isspace((unsigned char) str[len - 1])) + len--; + + if (parse_bool_with_len(str, len, &result)) + PG_RETURN_BOOL(result); + + ereturn(fcinfo->context, 0, + (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("invalid input syntax for type boolean: \"%s\"", in_str))); + + /* not reached */ + PG_RETURN_BOOL(false); +''' + + +def find_functions_with_call(functions): + ddd = '|'.join(functions + ['ereturn']) + call_pattern = f'(?:{ddd})' + function_with_call_pattern = create_pattern(call_pattern) + + caller_functions = [] + caller_pg_functions = [] + + unadapted_functions = {} + unadapted_calls = [] + functions_with_unadapted_calls = {} + + for root, subdirs, files in os.walk('../..'): + for filename in files: + file_path = os.path.join(root, filename) + # if filename[-2:] == '.c': + if filename in source_filenames: + # print(file_path) + with open(file_path, 'r') as f: + content = f.read() + # content = 'lol () { print(\'sds\') ereturn(wwe); }' + matches = re.findall(function_with_call_pattern, content) + if len(matches) > 0: + for m in matches: + # print('!!!!!!!!!!', file_path, m[0], m[1], m[2], m[3], '$$$$$$$$', m[4]) + caller_functions += [m[1]] + if m[2] == 'PG_FUNCTION_ARGS': + caller_pg_functions += [m[1]] + + if (m[0] != 'bool' or re.search('escontext', m[2]) is None) and m[2] != 'PG_FUNCTION_ARGS': + unadapted_functions[m[1]] = True + + if m[5] != 'ereturn': + func_call = m[4][-20:] + m[5] + m[6][:20] + safe_call_pattern = f'if{p_spaces}\(!{m[5]}\({p_any}\)\)' + safe_call_void_pattern = f'\(void\){p_spaces}{m[5]}\({p_any}\)\)' + direct_safe_call_pattern = f'DirectFunctionCall1Safe\({m[5]}' + if re.search(safe_call_pattern, func_call) is None and \ + re.search(safe_call_void_pattern, func_call) is None and \ + re.search(direct_safe_call_pattern, func_call) is None: + unadapted_calls += [m[5]] + + if m[1] in functions_with_unadapted_calls: + functions_with_unadapted_calls[m[1]] += [m[5]] + else: + functions_with_unadapted_calls[m[1]] = [m[5]] + + return caller_functions, caller_pg_functions, unadapted_functions, unadapted_calls, functions_with_unadapted_calls + +caller_functions = [] + +# print(re.findall('!' + p_any + '\n*!', f'!{boolin}!')) + +while True: + new_caller_functions, new_caller_pg_functions, unadapted_functions, unadapted_calls, functions_with_unadapted_calls = find_functions_with_call(caller_functions) + + if len(caller_functions) == len(new_caller_functions): + break + + # for c in sorted(new_caller_functions): + # if c not in caller_functions: + # print(c) + + for f in unadapted_functions: + print(f) + + caller_functions = new_caller_functions + + print(len(caller_functions), len(unadapted_functions), len(unadapted_calls), len(functions_with_unadapted_calls)) + +# replase res = func(intput) -> if (!func(input, &res, escontext)) return false; +# define safe_call(functionCall, input...) if (!functionCall(input)) return false; + +for f in sorted(functions_with_unadapted_calls): + print(f, functions_with_unadapted_calls[f]) \ No newline at end of file diff --git a/contrib/try_convert/scripts/find_calls.py b/contrib/try_convert/scripts/find_calls.py new file mode 100644 index 00000000000..328307bd911 --- /dev/null +++ b/contrib/try_convert/scripts/find_calls.py @@ -0,0 +1,116 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import re +import os, glob + +from general import source_filenames + +p_any = '(?:\S| |\t|\n+\t|\n+ |\n+#|\n+/|\n+\$)+?' +p_spaces = '\s*' +p_space = '\s+' + + +def remove_comments(body): + body = re.sub('".*?"', '""', body) + body = re.sub('/\*[\s\S]*?\*/', '/* */', body) + return body + +def find_functions(func_names, text): + + func_names_pattern_list = '|'.join(func_names) + func_names_pattern = f'((?:{func_names_pattern_list}))' + + func_pattern = '\n(\w+)\s+?' + func_names_pattern + p_spaces + '\((' + p_any + ')\)' + p_spaces + '(\n\{' + '(' + p_any + ')' + '\n+\})' + + funcs = [] + + for m in re.findall(func_pattern, text): + # print(m[1]) + funcs += [(m[1], m[0], m[2], m[3])] + + + return funcs + +def find_safe_functions(text): + + func_pattern = '\n((?:\w+\s+)+)(\w+)' + p_spaces + '\((' + p_any + 'Node' + p_spaces + '\*' + p_spaces + 'escontext' + ')\)' + p_spaces + '(\n\{' + '(' + p_any + ')' + '\n+\})' + + funcs = [] + + for m in re.findall(func_pattern, text): + # print(m[1]) + funcs += [(m[1], m[0], m[2], m[3])] + + + return funcs + +def create_pattern(funcCall): + return '\n((?:\w+\s+)+)(\w+)' + p_spaces + '\((' + p_any + ')\)' + p_spaces + '(\n\{' + '(' + p_any + ')' + '\W' + f'({funcCall})' + '\W' + '(' + p_any + ')' + '\n+\})' + + +def find_functions_with_call(functions): + ddd = '|'.join(functions) + call_pattern = f'(?:{ddd})' + function_with_call_pattern = create_pattern(call_pattern) + + caller_functions = [] + + for root, subdirs, files in os.walk('../..'): + for filename in files: + file_path = os.path.join(root, filename) + # if filename[-2:] == '.c': + if filename in source_filenames: + # print(file_path) + with open(file_path, 'r') as f: + content = remove_comments(f.read()) + # content = 'lol () { print(\'sds\') ereturn(wwe); }' + matches = re.findall(function_with_call_pattern, content) + if len(matches) > 0: + for m in matches: + # print('!!!!!!!!!!', file_path, m[0], m[1], m[2], m[3], '$$$$$$$$', m[4]) + # if m[1] == 'DecodeNumberField': + # print(m[1], m[4], m[5]) + if m[1] not in ['if']: + caller_functions += [m[1]] + + return caller_functions + + +def get_all_functions_with(function_call): + + + caller_functions = {function_call} + + # print(re.findall('!' + p_any + '\n*!', f'!{boolin}!')) + + while True: + new_caller_functions = find_functions_with_call(list(caller_functions)) + + l = len(caller_functions) + + for cf in new_caller_functions: + caller_functions.add(cf) + + if l == len(caller_functions): + break + + # print(len(caller_functions)) + + return list(caller_functions) \ No newline at end of file diff --git a/contrib/try_convert/scripts/find_casts.py b/contrib/try_convert/scripts/find_casts.py new file mode 100644 index 00000000000..23e20c52f41 --- /dev/null +++ b/contrib/try_convert/scripts/find_casts.py @@ -0,0 +1,248 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import re +import os, glob + + +top_srcdir = '../..' + +pg_type_path = f'{top_srcdir}/src/include/catalog/pg_type.dat' +pg_cast_path = f'{top_srcdir}/src/include/catalog/pg_cast.dat' +pg_proc_path = f'{top_srcdir}/src/include/catalog/pg_proc.dat' + + +def get_pg_proc(): + f = open(pg_proc_path) + content = f.read() + + # { oid => '42', descr => 'I/O', + # proname => 'int4in', prorettype => 'int4', proargtypes => 'cstring', + # prosrc => 'int4in' }, + # func_pattern = r'DATA\(insert OID =\s+(\w*)\s+\(.*?(\w*) _null_ _null_ _null_ n?\s?a?\s?\)\);'; + func_pattern = r"\{ oid => '(\w*)',[\s\S]*?prosrc => '(\w*)'[\s\S]*?\}"; + + func_id_name = {} + + # func_id_name['0'] = 'via I/O' + + for (id, name) in re.findall(func_pattern, content): + func_id_name[id] = name + + print('func_id_name', len(func_id_name)) + + return func_id_name + + +def get_pg_type(): + f = open(pg_type_path) + content = f.read() + + # { oid => '23', array_type_oid => '1007', + # descr => '-2 billion to 2 billion integer, 4-byte storage', + # typname => 'int4', typlen => '4', typbyval => 't', typcategory => 'N', + # typinput => 'int4in', typoutput => 'int4out', typreceive => 'int4recv', + # typsend => 'int4send', typalign => 'i' }, + type_pattern = r"\{ oid => '(\w*)',[\s\S]*?typname => '(\w*)'[\s\S]*?typinput => '(\w*)'[\s\S]*?typoutput => '(\w*)'[\s\S]*?\}"; + + type_name_id = {} + type_id_name = {} + type_io_funcs = {} + + for t in re.findall(type_pattern, content): + id = t[0] + name = t[1] + infunc = t[2] + outfunc = t[3] + + type_io_funcs[name] = [infunc, outfunc] + # print(len(t), t[3]) + if name != '' and name[0] != '_': + id = int(id) + type_id_name[id] = name + type_name_id[name] = id + + print('type_name_id', len(type_name_id)) + + return type_name_id, type_id_name, type_io_funcs + + +def get_pg_cast(type_id_name, func_id_name): + f = open(pg_cast_path) + content = f.read() + +# { castsource => 'int8', casttarget => 'int4', castfunc => 'int4(int8)', +# castcontext => 'a', castmethod => 'f' }, + # cast_pattern = r'DATA\(insert \([\s]*(\d+)[\s]+(\d+)[\s]+(\d+)[\s]+(.)[\s]+(.)'; + cast_pattern = r"\{ castsource => '(\w+)', casttarget => '(\w+)', castfunc => '(\w+)',\s*castcontext => '(\w+)', castmethod => '(\w+)' \}"; + + casts = [] + + for (source, target, funcid, _, meth) in re.findall(cast_pattern, content): + # if int(source) not in type_id_name or int(target) not in type_id_name: + # continue + + way = '______unknown' + + if meth == 'f': + if funcid not in func_id_name: + way = '______unknown_funcid' + else: + way = func_id_name[funcid] + if way == '': + way = '______unknown_funcid' + + elif meth == 'i': + way = 'WITH INOUT' + elif meth == 'b': + way = 'WITHOUT FUNCTION' + + + + + casts += [(source, target, way)] + # print(type_id_name[int(source)], ' -> ', type_id_name[int(target)], ' via ', meth, f'({funcid} - {func_id_name[funcid]}) ', f'{source}-{target}') + + print('casts', len(casts)) + + return casts + + +### GET FROM TEXT (EXTENSIONS) + +from general import supported_extensions + +def get_extensions(): + + create_casts = [] + create_functions = [] + + for extension in supported_extensions: + for root, subdirs, files in os.walk(f'../{extension}'): + for filename in files: + file_path = os.path.join(root, filename) + if filename[-4:] == '.sql': + # print(file_path) + with open(file_path, 'r') as f: + content = f.read() + + create_casts += find_create_casts_in_text(content) + + create_functions += list(find_create_function_in_text(content).items()) + + return create_casts, dict(create_functions) + +def find_create_casts_in_text(text): + create_cast_pattern = 'CREATE CAST\s*\((\w+) AS (\w+)\)\s*([\w\s\(\)]+);' + create_casts = [] + + for target, source, f in re.findall(create_cast_pattern, text): + # print(target, source, f) + if re.match('WITH INOUT', f) is not None: + create_casts += [(target, source, 'WITH INOUT')] + if re.match('WITHOUT FUNCTION', f) is not None: + create_casts += [(target, source, 'WITHOUT FUNCTION')] + + m = re.match('WITH FUNCTION ([\w\(\)]+)', f) + if m is not None: + # print(m[1]) + create_casts += [(target, source, m[1])] + + return create_casts + +p_space = '\s+' + +def find_create_function_in_text(text): + create_function_pattern = 'CREATE FUNCTION' + p_space + '(\w+)\([\w\s,]+\)' + p_space + 'RETURNS' + p_space + '\w+' + p_space + 'AS' + p_space + '([\',\w\s]+)' + p_space + 'LANGUAGE[\w\s,]+;' + create_functions = {} + + for sql_name, c_obj in re.findall(create_function_pattern, text): + # print(target, source, f) + + m = re.fullmatch("\'(\w+)\'", c_obj) + if m is not None: + c_name = m[1] + create_functions[sql_name] = c_name + continue + + m = re.fullmatch("\'MODULE_PATHNAME\',\s+\'(\w+)\'", c_obj) + if m is not None: + c_name = m[1] + create_functions[sql_name] = c_name + + + return create_functions + + + +EXAMPLE_CRETATE_CAST_EXTENSION = ''' +# CITEXT + +CREATE CAST (citext AS text) WITHOUT FUNCTION AS IMPLICIT; +CREATE CAST (citext AS varchar) WITHOUT FUNCTION AS IMPLICIT; +CREATE CAST (citext AS bpchar) WITHOUT FUNCTION AS ASSIGNMENT; +CREATE CAST (text AS citext) WITHOUT FUNCTION AS ASSIGNMENT; +CREATE CAST (varchar AS citext) WITHOUT FUNCTION AS ASSIGNMENT; +CREATE CAST (bpchar AS citext) WITH FUNCTION citext(bpchar) AS ASSIGNMENT; +CREATE CAST (boolean AS citext) WITH FUNCTION citext(boolean) AS ASSIGNMENT; +CREATE CAST (inet AS citext) WITH FUNCTION citext(inet) AS ASSIGNMENT; + +CREATE FUNCTION citext(bpchar) +RETURNS citext +AS 'rtrim1' +LANGUAGE internal IMMUTABLE STRICT; + +CREATE FUNCTION citext(boolean) +RETURNS citext +AS 'booltext' +LANGUAGE internal IMMUTABLE STRICT; + +CREATE FUNCTION citext(inet) +RETURNS citext +AS 'network_show' +LANGUAGE internal IMMUTABLE STRICT; + + + +# HSTORE + +CREATE CAST (text[] AS hstore) + WITH FUNCTION hstore(text[]); + +CREATE CAST (hstore AS json) + WITH FUNCTION hstore_to_json(hstore); + +CREATE CAST (hstore AS jsonb) + WITH FUNCTION hstore_to_jsonb(hstore); + +CREATE FUNCTION hstore(text[]) +RETURNS hstore +AS 'MODULE_PATHNAME', 'hstore_from_array' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION hstore_to_json(hstore) +RETURNS json +AS 'MODULE_PATHNAME', 'hstore_to_json' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION hstore_to_jsonb(hstore) +RETURNS jsonb +AS 'MODULE_PATHNAME', 'hstore_to_jsonb' +LANGUAGE C IMMUTABLE STRICT; +''' \ No newline at end of file diff --git a/contrib/try_convert/scripts/find_ereturns.py b/contrib/try_convert/scripts/find_ereturns.py new file mode 100644 index 00000000000..d26d70bfafe --- /dev/null +++ b/contrib/try_convert/scripts/find_ereturns.py @@ -0,0 +1,67 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import re + +import os, glob + +from general import source_filenames + +p_any = '(?:\S| |\t|\n+\t|\n+ |\n+#|\n+/|\n+\$)+?' +p_anys = '(?:\S| |\t|\n+\t|\n+ |\n+#|\n+/|\n+\$)*?' +p_spaces = '\s*' +p_space = '\s+' + +ereturn_pattern = 'ereturn\(' + p_any + '\);' + +ereturns = {} + +for root, subdirs, files in os.walk('../..'): + for filename in files: + file_path = os.path.join(root, filename) + # if filename[-2:] == '.c': + if filename in source_filenames: + # print(file_path) + with open(file_path, 'r') as f: + content = f.read() + # content = 'lol () { print(\'sds\') ereturn(wwe); }' + matches = re.findall(ereturn_pattern, content) + + for m in matches: + + def get_field(field, m): + errfield = re.search(f'{field}\("('+ p_anys + '"' + p_anys + ')[\)\n]', m) + if errfield is not None: + errfield = errfield[1] + + return errfield + + errmsg = get_field('errmsg', m) + errdetail = get_field('errdetail', m) + errhint = get_field('errhint', m) + + desc = (errmsg, errdetail, errhint) + + ereturns[filename] = (ereturns[filename] if filename in ereturns else []) + [desc] + + +for filename in sorted(ereturns): + print(filename) + for ereturn in ereturns[filename]: + print(' ', ereturn) \ No newline at end of file diff --git a/contrib/try_convert/scripts/general.py b/contrib/try_convert/scripts/general.py new file mode 100644 index 00000000000..e0677e3e0d7 --- /dev/null +++ b/contrib/try_convert/scripts/general.py @@ -0,0 +1,136 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +source_filenames = [ + 'time.c', 'date.c', 'datetime.c', 'timestamp.c', 'nabstime.c', 'formatting.c', + 'int.c', 'int8.c', 'float.c', 'float.c', 'bool.c', 'numeric.c', 'numutils.c', + 'format_type.c', + # 'varbit.c', + 'char.c', 'varchar.c', + 'json.c', 'jsonb.c', 'xml.c', + 'network.c', + # 'cash.c', + 'reg_proc.c', + 'postgres.c', + 'stringinfo.c', + ] + [ + 'citext.c', 'oracle_compat.c', + 'hstore_io.c', + ] + +supported_types = [ + 'int8', # NUMBERS + 'int4', + 'int2', + 'float8', + 'float4', + 'numeric', + # 'complex', + + 'bool', + + # 'bit', # BITSTRING + # 'varbit', + + 'date', # TIME + 'time', + 'timetz', + 'timestamp', + 'timestamptz', + 'interval', + + # 'point', # GEOMENTY + # 'circle', + # 'line', + # 'lseg', + # 'path', + # 'box', + # 'polygon', + + # 'cidr', # IP + # 'inet', + # 'macaddr', + + 'json', # OBJ + 'jsonb', + # 'xml', + + # 'bytea', + + 'char', # STRINGS + # 'bpchar', + 'varchar', + 'text', + + # 'money', + # # 'pg_lsn', + # # 'tsquery', + # # 'tsvector', + # # 'txid_snapshot', + # 'uuid', + + # 'regtype', # SYSTEM + # 'regproc', + # 'regclass', + # 'oid', +] + [ + 'citext', + 'hstore', +] + +supported_extensions = [ + 'citext', + 'hstore', +] + + + +string_types = [ + 'text', + 'citext', + 'char', + # 'bpchar', + 'varchar', +] + +typmod_types = [ + 'bit', + 'varbit', + 'char', + 'varchar', + # 'bpchar', +] + +typmod_lens = [ + None, 1, 5, 10, 20 +] + + + +uncomparable_types = [ + 'json', + 'xml', + 'point', +] + +has_corrupt_data = [ + 'time', 'timetz', 'timestamp', 'timestamptz', 'date', + 'json', + 'int2', 'int4', 'int8', 'float4', 'float8', 'numeric', +] \ No newline at end of file diff --git a/contrib/try_convert/scripts/generate_data.py b/contrib/try_convert/scripts/generate_data.py new file mode 100644 index 00000000000..b4aa4aec941 --- /dev/null +++ b/contrib/try_convert/scripts/generate_data.py @@ -0,0 +1,119 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import random +import datetime +import time +import pytz + +random.seed(42) + +### NUMBERS + +numbers = { + 'int2' : ((-32768, 32767), False), + 'int4' : ((-2147483648, 2147483647), False), + 'int8' : ((-9223372036854775808, 9223372036854775807), False), + 'float4' : ((10**6, 10**6), True), + 'float8' : ((10**15, 10**15), True), + 'numeric' : ((10**20, 10**20), True), +} + + +def save_datafile(t, data): + filename = f'data/tt_{t}.data' + file = open(filename, 'w') + file.write('\n'.join([str(d) for d in data])) + + return filename + + +for number_type in numbers: + + type_range = numbers[number_type][0] + is_float = numbers[number_type][1] + + mn = type_range[0] + mx = type_range[1] + + nln = 0 + ln = len(str(type_range[1]))-1 + + table_name = f'tt_{number_type}' + + values = [] + + for c in range(nln, ln): + rep = 20 // ln + 1 + for _ in range(rep): + n = random.random() + if c >= 0: + n *= (10 ** (c+1)) + else: + n /= (10 ** (-c)) + if not is_float: + n = int(n) + + values += [n] + + filename = save_datafile(number_type, values) + +### TIMES + +MINTIME = datetime.datetime.fromtimestamp(0) +MAXTIME = datetime.datetime(2024,12,2,10,39,59) +mintime_ts = int(time.mktime(MINTIME.timetuple())) +maxtime_ts = int(time.mktime(MAXTIME.timetuple())) + +timestamp_values = [] +timestamptz_values = [] +time_values = [] +timetz_values = [] +date_values = [] +interval_values = [] + +timezones = [pytz.timezone("UTC"), pytz.timezone("Asia/Istanbul"), pytz.timezone("US/Eastern"), pytz.timezone("Asia/Tokyo")] + +for _ in range(12): + random_ts = random.randint(mintime_ts, maxtime_ts) + randtom_tz = timezones[random.randint(0, len(timezones)-1)] + RANDOMTIME = datetime.datetime.fromtimestamp(random_ts, randtom_tz) + R_clear = datetime.datetime.fromtimestamp(RANDOMTIME.timestamp()) + + timestamp_text = str(R_clear) + timestamptz_text = str(RANDOMTIME) + timetz_text = str(RANDOMTIME.time()) + " " + RANDOMTIME.tzname() + time_text = str(RANDOMTIME.time()) + date_text = str(RANDOMTIME.date()) + dt = (R_clear-MINTIME) + interval_text = str(dt) + + timestamp_values += [timestamp_text] + timestamptz_values += [timestamptz_text] + time_values += [time_text] + timetz_values += [timetz_text] + date_values += [date_text] + interval_values += [interval_text] + +save_datafile("timestamp", timestamp_values) +save_datafile("timestamptz", timestamptz_values) +save_datafile("time", time_values) +save_datafile("timetz", timetz_values) +save_datafile("date", date_values) +save_datafile("interval", interval_values) diff --git a/contrib/try_convert/scripts/generate_test.py b/contrib/try_convert/scripts/generate_test.py new file mode 100644 index 00000000000..7b17be3d7c4 --- /dev/null +++ b/contrib/try_convert/scripts/generate_test.py @@ -0,0 +1,689 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import re + +from general import supported_types, string_types, typmod_types, typmod_lens + +def get_typemod_type(t, l): + if l is None: + return t + else: + return f'{t}({l})' + +def get_typemod_table(t, l): + if l is None: + return f'tt_{t}' + else: + return f'tt_{t}_{l}' + + +from general import uncomparable_types, has_corrupt_data + +from general import supported_extensions + + +print('Supported types:', ' '.join(supported_types)) + + +def remove_empty_lines(t): + return "\n".join([s for s in t.split("\n") if s]) + +### GET FUNCTION IDs + +from find_casts import get_pg_proc + +func_id_name = get_pg_proc() + + +### GET TYPE IDs + +from find_casts import get_pg_type + +type_name_id, type_id_name, _ = get_pg_type() + +supported_types_count = 0 + +print(f'Types found: {len(type_id_name)}, supported: {supported_types_count}') + + +### GET CONVERTS + +from find_casts import get_pg_cast + +casts = get_pg_cast(type_id_name, func_id_name) + +supported_cast_count = 0 + +print(f'Casts found: {len(casts)}, supported: {supported_cast_count}') + + +### HEADER & FOOTER + +test_header = \ + f'-- SCRIPT-GENERATED TEST for TRY_CONVERT\n' \ + f'-- Tests {supported_types_count} types of {len(type_id_name)} from pg_types.h\n' \ + f'-- Tests {supported_cast_count} cast of {len(casts)} from pg_cast.h\n' \ + f'create schema tryconvert;\n' \ + f'set search_path = tryconvert;\n' \ + f'-- start_ignore\n' \ + f'CREATE EXTENSION IF NOT EXISTS try_convert;\n' \ + f'-- end_ignore\n' + +for extension in supported_extensions: + test_header += \ + f'-- start_ignore\n' \ + f'CREATE EXTENSION IF NOT EXISTS {extension};\n' \ + f'-- end_ignore\n' + +test_header_out = test_header + +for type_name in supported_types: + add = \ + f'select add_type_for_try_convert(\'{type_name}\'::regtype);\n' + + out = \ + ' add_type_for_try_convert \n' \ + '--------------------------\n' \ + ' \n' \ + '(1 row)\n' \ + '\n' + + test_header += add + + test_header_out += add + out + +test_header_out = test_header_out[:-1] + +test_footer = 'reset search_path;' + + +### TRY_CONVERT_BY_SQL + +test_funcs = '' + +func_text = \ + f'CREATE FUNCTION try_convert_by_sql_text(_in text, INOUT _out ANYELEMENT, source_type text)\n' \ + f' LANGUAGE plpgsql AS\n' \ + f'$func$\n' \ + f' BEGIN\n' \ + f' EXECUTE format(\'SELECT %L::%s::%s\', $1, source_type, pg_typeof(_out))\n' \ + f' INTO _out;\n' \ + f' EXCEPTION WHEN others THEN\n' \ + f' -- do nothing: _out already carries default\n' \ + f' END\n' \ + f'$func$;\n' + +test_funcs += func_text + +func_text = \ + f'CREATE FUNCTION try_convert_by_sql_text_with_len_out(_in text, INOUT _out ANYELEMENT, source_type text, len_out int)\n' \ + f' LANGUAGE plpgsql AS\n' \ + f'$func$\n' \ + f' BEGIN\n' \ + f' EXECUTE format(\'SELECT %L::%s::%s(%s)\', $1, source_type, pg_typeof(_out), len_out::text)\n' \ + f' INTO _out;\n' \ + f' EXCEPTION WHEN others THEN\n' \ + f' -- do nothing: _out already carries default\n' \ + f' END\n' \ + f'$func$;\n' + +test_funcs += func_text + +for type_name in supported_types: + + func_text = \ + f'CREATE FUNCTION try_convert_by_sql_with_len_out(_in {type_name}, INOUT _out ANYELEMENT, len_out int)\n' \ + f' LANGUAGE plpgsql AS\n' \ + f'$func$\n' \ + f' BEGIN\n' \ + f' EXECUTE format(\'SELECT %L::{type_name}::%s(%s)\', $1, pg_typeof(_out), len_out::text)\n' \ + f' INTO _out;\n' \ + f' EXCEPTION WHEN others THEN\n' \ + f' -- do nothing: _out already carries default\n' \ + f' END\n' \ + f'$func$;\n' + + test_funcs += func_text + + func_text = \ + f'CREATE FUNCTION try_convert_by_sql(_in {type_name}, INOUT _out ANYELEMENT)\n' \ + f' LANGUAGE plpgsql AS\n' \ + f'$func$\n' \ + f' BEGIN\n' \ + f' EXECUTE format(\'SELECT %L::{type_name}::%s\', $1, pg_typeof(_out))\n' \ + f' INTO _out;\n' \ + f' EXCEPTION WHEN others THEN\n' \ + f' -- do nothing: _out already carries default\n' \ + f' END\n' \ + f'$func$;\n' + + test_funcs += func_text + +for source_type in supported_types: + for target_type in supported_types: + for source_typmod in typmod_lens: + if source_type not in typmod_types and source_typmod is not None: + continue + for target_typmod in typmod_lens: + if target_type not in typmod_types and target_typmod is not None: + continue + + source_name = source_type + if source_typmod is not None: + source_name += f'({source_typmod})' + + target_name = target_type + if target_typmod is not None: + target_name += f'({target_typmod})' + + + func_text = \ + f'CREATE FUNCTION try_convert_by_exception_{source_typmod}_{target_typmod}(_in {source_name}, d {target_name}) RETURNS {target_name}\n' \ + f' LANGUAGE plpgsql AS\n' \ + f'$func$\n' \ + f' BEGIN\n' \ + f' RETURN CAST(_in AS {target_name});\n' \ + f' EXCEPTION WHEN others THEN\n' \ + f' RETURN d;\n' \ + f' END\n' \ + f'$func$;\n' + + test_funcs += func_text + +### CREATE DATA + +test_load_data = '-- LOAD DATA\n' + +test_load_data += f'CREATE TABLE tt_temp (v text) DISTRIBUTED BY (v);\n' + +def copy_data(table_name, filename, type_name): + return f'DELETE FROM tt_temp;\n' \ + f'COPY tt_temp from \'@abs_srcdir@/{filename}\';\n' \ + f'INSERT INTO {table_name}(id, v) SELECT row_number() OVER(), v::{type_name} from tt_temp;' + +type_tables = {} + +def create_table(type_name, varlen=None): + table_name = get_typemod_table(type_name, varlen) + field_type = get_typemod_type(type_name, varlen) + + type_tables[type_name] = table_name + + load_data = f'CREATE TABLE {table_name} (id serial, v {field_type}) DISTRIBUTED BY (id);\n' + + filename = f'data/tt_{type_name}.data' + + load_data += copy_data(table_name, filename, field_type) + '\n' + + # load_data += f'SELECT * FROM {table_name};' + + return load_data + +def get_string_table(type_name, string_type, type_varlen=None, string_varlen=None): + + if type_varlen is not None and string_varlen is not None: + return f'tt_{string_type}_{string_varlen}_of_{type_name}_{type_varlen}' + elif type_varlen is not None: + return f'tt_{string_type}_of_{type_name}_{type_varlen}' + elif string_varlen is not None: + return f'tt_{string_type}_{string_varlen}_of_{type_name}' + + return f'tt_{string_type}_of_{type_name}' + +for type_name in supported_types: + + for type_varlen in typmod_lens: + if type_varlen is not None and type_name not in typmod_types: + continue + + test_load_data += create_table(type_name, type_varlen) + + for string_type in string_types: + for string_varlen in typmod_lens: + if string_varlen is not None and string_type not in typmod_types: + continue + + field_type = get_typemod_type(type_name, type_varlen) + string_field_type = get_typemod_type(string_type, string_varlen) + + table_name = get_string_table(type_name, string_type, type_varlen, string_varlen) + + load_data = f'CREATE TABLE {table_name} (id serial, v {string_field_type}) DISTRIBUTED BY (id);\n' + + cut = f'::{field_type}' if type_varlen is not None else '' + + load_data += f'INSERT INTO {table_name}(id, v) SELECT row_number() OVER(), v{cut}::{string_field_type} from tt_temp;\n' + + test_load_data += load_data + + if type_name in has_corrupt_data: + + for string_type in string_types: + for string_varlen in typmod_lens: + if string_varlen is not None and string_type not in typmod_types: + continue + + field_type = get_typemod_type(type_name, type_varlen) + string_field_type = get_typemod_type(string_type, string_varlen) + + corr_table_name = 'corr_' + get_string_table(type_name, string_type, type_varlen, string_varlen) + + load_data = f'CREATE TABLE {corr_table_name} (id serial, v {string_field_type}) DISTRIBUTED BY (id);\n' + + filename = f'data/corr_{type_name}.data' + + load_data += copy_data(corr_table_name, filename, string_field_type) + '\n' + + test_load_data += load_data + + + + +## GET DATA + +def get_data(type_name): + return type_tables[type_name] + +def get_len_from_data(type_name): + f = open(f'data/tt_{type_name}.data') + return(len(f.read().split('\n'))) + +def get_len_from_corr_data(type_name): + f = open(f'data/corr_{type_name}.data') + return(len(f.read().split('\n'))) + +def get_from_data(type_name, i = None): + f = open(f'data/tt_{type_name}.data') + values = f.read().split('\n') + if i is None: + return content + return values[i] + +## TEST + +def create_test(source_name, target_name, test_data, default='NULL', source_varlen=None, target_varlen=None, source_count=0): + + test_filter = 'v1 is distinct from v2' if target_name not in uncomparable_types else 'v1::text is distinct from v2::text' + test_filter_not = 'v1 is not distinct from v2' if target_name not in uncomparable_types else 'v1::text is not distinct from v2::text' + + target_name_1 = get_typemod_type(target_name, target_varlen) + + try_convert_sql = f'try_convert_by_exception_{source_varlen}_{target_varlen}(v, {default}::{target_name_1})' + + query = \ + f'select * from (' \ + f'select ' \ + f'try_convert(v, {default}::{target_name_1}) as v1, ' \ + f'{try_convert_sql} as v2' \ + f', v' \ + f' from {test_data}' \ + f') as t(v1, v2, v) where {test_filter};' + result = \ + ' v1 | v2 | v \n' \ + '----+----+---\n' \ + '(0 rows)\n' + + query_not = \ + f'select count(*) from (' \ + f'select ' \ + f'try_convert(v, {default}::{target_name_1}) as v1, ' \ + f'{try_convert_sql} as v2' \ + f' from {test_data}' \ + f') as t(v1, v2) where {test_filter_not};' + result_not = \ + ' count \n' \ + '-------\n' \ + f' {source_count}\n' \ + '(1 row)\n' + + input_source = query + '\n' + query_not + output_source = remove_empty_lines(query) + '\n' + result + '\n' + remove_empty_lines(query_not) + '\n' + result_not + + return input_source, output_source + + +### CAST to & from text + +text_tests_in = [] +text_tests_out = [] + +default_value = 'NULL' + +for string_type in string_types: + for string_varlen in typmod_lens: + if string_varlen is not None and type_name not in typmod_types: + continue + + for type_name in supported_types: + for type_varlen in typmod_lens: + if type_varlen is not None and type_name not in typmod_types: + continue + + test_type_table = get_typemod_table(type_name, type_varlen) + + text_type_table = get_string_table(type_name, string_type, type_varlen, string_varlen) + + test_corrupted_text_data = f'(select (\'!@#%^&*\' || v || \'!@#%^&*\') from {text_type_table}) as t(v)' + + data_count = get_len_from_data(type_name) + + to_text_in, to_text_out = create_test( + type_name, string_type, + test_type_table, default_value, + type_varlen, string_varlen, + data_count + ) + from_text_in, from_text_out = create_test( + string_type, type_name, + text_type_table, default_value, + string_varlen, type_varlen, + data_count + ) + from_corrupted_text_in, from_corrupted_text_out = create_test( + string_type, type_name, + test_corrupted_text_data, default_value, + string_varlen, type_varlen, + data_count + ) + + text_tests_in += [to_text_in, from_text_in] + text_tests_out += [to_text_out, from_text_out] + + text_tests_in += [from_corrupted_text_in] + text_tests_out += [from_corrupted_text_out] + + data_count = get_len_from_data(type_name) + + if type_name in has_corrupt_data: + + corr_text_type_table = 'corr_' + text_type_table + + data_count = get_len_from_corr_data(type_name) + + from_corr_in, from_corr_out = create_test( + string_type, type_name, + corr_text_type_table, default_value, + string_varlen, type_varlen, + data_count + ) + + text_tests_in += [from_corr_in] + text_tests_out += [from_corr_out] + + +# print(text_tests_in[0]) +# print(text_tests_in[1]) + + +### CAST from pg_cast + +function_tests_in = [] +function_tests_out = [] + +type_casts = [(source_name, target_name) for (source_name, target_name, method) in casts] + +for source_name, target_name in type_casts: + if (source_name not in supported_types or target_name not in supported_types): + continue + + dd = get_from_data(target_name, 0).translate(str.maketrans('', '', '\'')) + d = f'\'{dd}\'' + + for default in ['NULL', d]: + + for source_varlen in typmod_lens: + if source_varlen is not None and source_name not in typmod_types: + continue + + test_table = get_typemod_table(source_name, source_varlen) + + for target_varlen in typmod_lens: + if target_varlen is not None and target_name not in typmod_types: + continue + + data_count = get_len_from_data(source_name) + + test_in, test_out = create_test( + source_name, target_name, + test_table, default, + source_varlen, target_varlen, + data_count + ) + + function_tests_in += [test_in] + function_tests_out += [test_out] + + +# print(function_tests_in[0]) + + +### DEFAULTS TEST + +# for type_name in supported_types: + +# query = f'SELECT try_convert({}::{}, {get_from_data(type_name, 0)}::{type_name});' + +### ONE MILLION ERRORS + +test_million = '' + +test_million_data = \ + 'DROP TABLE IF EXISTS text_ints; CREATE TABLE text_ints (v text) DISTRIBUTED BY (v);\n' \ + 'INSERT INTO text_ints(v) SELECT (random()*1000)::int4::text FROM generate_series(1,1000000);\n' \ + 'DROP TABLE IF EXISTS text_error_ints; CREATE TABLE text_error_ints (v text) DISTRIBUTED BY (v);\n' \ + 'INSERT INTO text_error_ints(v) SELECT (random()*1000000 + 1000000)::int8::text FROM generate_series(1,1000000);\n' \ + 'DROP TABLE IF EXISTS int4_ints; CREATE TABLE int4_ints (v int4) DISTRIBUTED BY (v);\n' \ + 'INSERT INTO int4_ints(v) SELECT (random()*1000)::int4 FROM generate_series(1,1000000);\n' \ + 'DROP TABLE IF EXISTS int4_error_ints; CREATE TABLE int4_error_ints (v int4) DISTRIBUTED BY (v);\n' \ + 'INSERT INTO int4_error_ints(v) SELECT (random()*1000000 + 1000000)::int4 FROM generate_series(1,1000000);\n' + +test_million_query1 = \ + 'SELECT count(*) FROM (SELECT try_convert(v, NULL::int2) as v FROM text_ints) as t(v) WHERE v IS NOT NULL;\n' +test_million_query2 = \ + 'SELECT count(*) FROM (SELECT try_convert(v, NULL::int2) as v FROM text_error_ints) as t(v) WHERE v IS NULL;\n' + +test_million_query3 = \ + 'SELECT count(*) FROM (SELECT try_convert(v, NULL::int2) as v FROM int4_ints) as t(v) WHERE v IS NOT NULL;\n' +test_million_query4 = \ + 'SELECT count(*) FROM (SELECT try_convert(v, NULL::int2) as v FROM int4_error_ints) as t(v) WHERE v IS NULL;\n' + +test_million_result = \ + ' count \n' \ + '---------\n' \ + ' 1000000\n' \ + '(1 row)\n' \ + +test_million_in = test_million_data + test_million_query1 + test_million_query2 + test_million_query3 + test_million_query4 +test_million_out = test_million_data + \ + test_million_query1 + test_million_result + '\n' + \ + test_million_query2 + test_million_result + '\n' + \ + test_million_query3 + test_million_result + '\n' + \ + test_million_query4 + test_million_result + +### NESTED CASTS + +value = '42::int4' + +for level in range(100): + value = f'try_convert(try_convert({value}, NULL::text), NULL::int4)' + + +test_nested_query = f'select {value} as v;\n' + +test_nested_result = \ + ' v \n' \ + '----\n' \ + ' 42\n' \ + '(1 row)\n' \ + +test_nested_in = test_nested_query +test_nested_out = test_nested_query + test_nested_result + +edge_case_queries = [ + # --- NULL FALLBACK + NUMERIC --- + "select try_convert('42d'::text, NULL::numeric(38,2)) is null as r;", + "select try_convert('42d'::text, 0::numeric(38,2)) = 0 as r;", + "select try_convert('42.123'::text, NULL::numeric(38,2)) = 42.12 as r;", + + # --- NULL source --- + "select try_convert(NULL::text, 42::int) is null as r;", + "select try_convert(NULL::text, NULL::int) is null as r;", + "select try_convert(NULL::int, 7::int) is null as r;", + + # --- Fallback value must respect target typmod (regression for is_failed reset) --- + "select try_convert('bad'::text, 3.14159::numeric(10,2)) = 3.14 as r;", + "select try_convert('bad'::text, 1::numeric(4,2)) = 1.00 as r;", + + # --- RELABEL path (same base type) --- + "select try_convert('hello'::text, NULL::text) = 'hello' as r;", + "select try_convert('abcdefgh'::varchar(20), NULL::varchar(5)) = 'abcde' as r;", + "select try_convert(42.567::numeric(10,3), NULL::numeric(5,1)) = 42.6 as r;", + + # --- Numeric overflow / rounding / typmod --- + "select try_convert('99999.999'::text, NULL::numeric(5,2)) is null as r;", + "select try_convert('-99999.999'::text, NULL::numeric(5,2)) is null as r;", + "select try_convert('0.001'::text, NULL::numeric(5,2)) = 0.00 as r;", + "select try_convert('0.005'::text, NULL::numeric(5,2)) = 0.01 as r;", + + # --- Empty / whitespace strings --- + "select try_convert(''::text, NULL::int) is null as r;", + "select try_convert(''::text, 0::int) = 0 as r;", + "select try_convert(' '::text, 0::int) = 0 as r;", + "select try_convert(' 42 '::text, NULL::int) = 42 as r;", + + # --- Signed numbers --- + "select try_convert('+42'::text, NULL::int) = 42 as r;", + "select try_convert('-42'::text, NULL::int) = -42 as r;", + + # --- Integer overflow / wide types --- + "select try_convert('99999999999'::text, NULL::int) is null as r;", + "select try_convert('99999999999'::text, NULL::bigint) = 99999999999 as r;", + "select try_convert('1e10'::text, NULL::bigint) is null as r;", + "select try_convert('1e100'::text, NULL::int) is null as r;", + + # --- Float special values --- + "select try_convert('NaN'::text, NULL::float8) = 'NaN'::float8 as r;", + "select try_convert('Infinity'::text, NULL::float8) = 'Infinity'::float8 as r;", + "select try_convert('-Infinity'::text, NULL::float8) = '-Infinity'::float8 as r;", + + # --- Date / time invalid inputs --- + "select try_convert('2026-13-01'::text, NULL::date) is null as r;", + "select try_convert('2026-02-30'::text, NULL::date) is null as r;", + "select try_convert('not-a-date'::text, NULL::date) is null as r;", + "select try_convert('2026-04-20'::text, NULL::date) = '2026-04-20'::date as r;", + + # --- Boolean parsing --- + "select try_convert('true'::text, NULL::bool) = true as r;", + "select try_convert('t'::text, NULL::bool) = true as r;", + "select try_convert('1'::text, NULL::bool) = true as r;", + "select try_convert('yes'::text, NULL::bool) = true as r;", + "select try_convert('maybe'::text, NULL::bool) is null as r;", + "select try_convert('maybe'::text, false::bool) = false as r;", + + # --- Nested calls --- + # '42.9' -> numeric succeeds, but int parser doesn't accept decimals -> NULL + "select try_convert(try_convert('42.9'::text, NULL::numeric)::text, NULL::int) is null as r;", + # Inner returns NULL; outer receives NULL source, returns NULL (not the fallback) + # This documents current behaviour: NULL source is short-circuited before fallback. + "select try_convert(try_convert('bad'::text, NULL::int)::text, -1::int) is null as r;", + + # --- JSON (if supported) --- + "select try_convert('{\"a\":1}'::text, NULL::json) is not null as r;", + "select try_convert('{bad json}'::text, NULL::json) is null as r;", +] + +test_edge_cases_result = \ + ' r \n' \ + '---\n' \ + ' t\n' \ + '(1 row)\n' + +test_edge_cases_in = '\n'.join(edge_case_queries) + '\n' +test_edge_cases_out = '\n'.join( + q + '\n' + test_edge_cases_result for q in edge_case_queries +) + + +### EDGE CASE: try_convert inside PL/pgSQL (SPI context regression) + +test_spi_in = \ + "DO $$\n" \ + "DECLARE\n" \ + " r int;\n" \ + "BEGIN\n" \ + " SELECT try_convert('42'::text, 0::int) INTO r;\n" \ + " IF r <> 42 THEN RAISE EXCEPTION 'expected 42, got %', r; END IF;\n" \ + " SELECT try_convert('bad'::text, -1::int) INTO r;\n" \ + " IF r <> -1 THEN RAISE EXCEPTION 'expected -1, got %', r; END IF;\n" \ + "END$$;" + +test_spi_out = test_spi_in + + +### CONSTRUCT TEST + +test_str = '\n'.join([ + test_header, \ + # FUNCTIONS + test_funcs, \ + # CREATE DATA + test_load_data, \ + '-- TEXT TESTS', \ + '\n'.join(text_tests_in), \ + '-- FUNCTION TESTS', \ + '\n'.join(function_tests_in), \ + '-- MILLION TESTS', \ + test_million_in, + '-- NESTED TESTS', \ + test_nested_in, + '-- EDGE CASES', \ + test_edge_cases_in, + '-- SPI CONTEXT', \ + test_spi_in, + test_footer + ]) + '\n' + +test_f = open('input/try_convert.source', 'w') +test_f.write(test_str) + + +test_str = '\n'.join([ + test_header_out, \ + # FUNCTIONS + remove_empty_lines(test_funcs), \ + # CREATE DATA + remove_empty_lines(test_load_data), \ + '-- TEXT TESTS', \ + '\n'.join(text_tests_out), \ + '-- FUNCTION TESTS', \ + '\n'.join(function_tests_out), \ + '-- MILLION TESTS', \ + test_million_out, + '-- NESTED TESTS', \ + test_nested_out, + '-- EDGE CASES', \ + test_edge_cases_out, + '-- SPI CONTEXT', \ + test_spi_out, + remove_empty_lines(test_footer) + ]) + '\n' + +test_f = open('output/try_convert.source', 'w') +test_f.write(test_str) diff --git a/contrib/try_convert/scripts/verify.py b/contrib/try_convert/scripts/verify.py new file mode 100644 index 00000000000..8eb8d0b65ae --- /dev/null +++ b/contrib/try_convert/scripts/verify.py @@ -0,0 +1,368 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +##################################################### +# # +# Verifies no ereport called while convertation # +# # +##################################################### + +# Need also to verify context forwarding + +import re +import os, glob + +DEBUG_FLAG = True +PRINT_ALL_FLAG = False + +from general import source_filenames +from general import supported_types + +def filter_supported_casts(casts): + supported_casts = [] + for cast in casts: + if cast[0] in supported_types and cast[1] in supported_types: + supported_casts += [cast] + return supported_casts + +# Get casts from pg_cast + +from find_casts import get_pg_cast, get_pg_proc, get_pg_type + +func_id_name = get_pg_proc() +type_name_id, type_id_name, type_io_funcs = get_pg_type() + +pg_casts = filter_supported_casts(get_pg_cast(type_id_name, func_id_name)) + +# Get casts from extensions + +from find_casts import get_extensions + + +extension_casts, extension_sql_functions = get_extensions() + +casts = pg_casts + extension_casts + + +if DEBUG_FLAG: + print(f'FOUND {len(casts)} CASTs') + if PRINT_ALL_FLAG: + for l in sorted(casts): + print(l) + + +# +# Get functions used to cast +# + +# Get INOUT functions + +required_in_funcs = {} +required_out_funcs = {} + +for cast in casts: + if cast[2] == 'WITH INOUT': + required_out_funcs[cast[0]] = True + required_in_funcs[cast[1]] = True + +if DEBUG_FLAG: + print(f'REQUIRED {len(required_in_funcs)} _IN FUNCTIONS') + if PRINT_ALL_FLAG: + for f in sorted(required_in_funcs): + print(f) + +if DEBUG_FLAG: + print(f'REQUIRED {len(required_out_funcs)} _OUT FUNCTIONS') + if PRINT_ALL_FLAG: + for f in sorted(required_out_funcs): + print(f) + + +# Get functions from CREATE CAST + +required_funcs = {} + +for cast in pg_casts: + if cast[2] != 'WITH INOUT' and cast[2] != 'WITHOUT FUNCTION': + required_funcs[cast[2]] = cast + + +from find_casts import find_create_function_in_text + +sql_funcs = extension_sql_functions + +# print(sql_funcs) + +for cast in extension_casts: + if cast[2] != 'WITH INOUT' and cast[2] != 'WITHOUT FUNCTION': + sql_func_name = cast[2] + + m = re.match('(\w+)\(', sql_func_name) + if m is not None: + sql_func_name = m[1] + + c_func = "Not found" + + if sql_func_name in sql_funcs: + c_func = sql_funcs[sql_func_name] + + # print(sql_func_name, c_func) + + required_funcs[c_func] = sql_func_name + +if DEBUG_FLAG: + print(f'REQUIRED {len(required_funcs)} FUNCTIONS FROM CREATE CAST') + if PRINT_ALL_FLAG: + for f in sorted(required_funcs): + print(f) + +required_funcs_list = list(required_funcs) + list(required_in_funcs) + list(required_out_funcs) + +for l in type_io_funcs: + if l in supported_types: + required_funcs_list += type_io_funcs[l] + +# +# Load functions bodies +# + +# load convert function + +from find_calls import find_functions + +convert_functions = [] + +for root, subdirs, files in os.walk('../..'): + for filename in files: + file_path = os.path.join(root, filename) + if filename[-2:] == '.c': + # if filename in source_filenames: + # print(file_path) + with open(file_path, 'r') as f: + content = f.read() + + funcs = find_functions(required_funcs_list, content) + + convert_functions += funcs + + # print(file_path, len(funcs)) + +loaded_convert_functions = {} + +from find_calls import remove_comments + +for name, return_type, args, body in convert_functions: + if return_type == 'Datum' and args == 'PG_FUNCTION_ARGS': + body = remove_comments(body) + loaded_convert_functions[name] = body + +for rf in required_funcs: + if rf not in loaded_convert_functions: + print(f'body for {rf}({required_funcs[rf]}) not found') + + +if DEBUG_FLAG: + print(f'FOUND {len(loaded_convert_functions)} FUNCTIONS BODIES') + +# load safe functions + +from find_calls import find_safe_functions + +safe_functions = [] + +null_functions = [] + +for root, subdirs, files in os.walk('../..'): + for filename in files: + file_path = os.path.join(root, filename) + if filename[-2:] == '.c': + # if filename in source_filenames: + # print(file_path) + with open(file_path, 'r') as f: + content = f.read() + + funcs = find_safe_functions(content) + + safe_functions += funcs + + +loaded_safe_functions = {} +loaded_null_functions = {} + +for name, return_type, args, body in safe_functions: + loaded_safe_functions[name] = remove_comments(body) + + if re.search(r'bool', return_type) is None: + print(f' WARNING: safe function {name} returns result not bool') + + m = re.match('(\w+)Safe', name) + if m is not None: + loaded_null_functions[m[1]] = 1 + + m = re.match('(\w+)_safe', name) + if m is not None: + loaded_null_functions[m[1]] = 1 + +if DEBUG_FLAG: + print(f'FOUND {len(loaded_safe_functions)} SAFE FUNCTIONS BODIES') + +if DEBUG_FLAG: + print(f'REQUIRED {len(loaded_null_functions)} SAFE FUNCTIONS VARIANTS') + +loaded_functions = dict(list(loaded_convert_functions.items()) + list(loaded_safe_functions.items())) + +# +# Check functions don't call unsafe ereport +# + +from find_calls import get_all_functions_with + +ereport_functions = get_all_functions_with('ereport\(ERROR,') + +unsafe_convert_functions = {} + +for func_name in loaded_functions: + body = loaded_functions[func_name] + + pattern_call = '(\w+)\s*\(([\s\S]*?)\)' + + for token_match in re.finditer(pattern_call, body): + + token = token_match[1] + + args = token_match[2] + + # if (token == 'ereport'): + # print(token, args[:5]) + + if token in ereport_functions or ((token == 'ereport' or token == 'elog') and args[:5] == 'ERROR'): + print(f' WARNING: call unsafe function {token} in {func_name}') + unsafe_convert_functions[func_name] = token + continue + +if DEBUG_FLAG: + print(f'FOUND {len(unsafe_convert_functions)} UNSAFE CONVERT FUNCTIONS') + + +# +# Check context forwarding don't call unsafe variants +# + +unsafe_variants = set(loaded_null_functions.keys()) + +unsafe_variant_usage = {} +unsafe_variant_usage_count = 0 +unwrapped_safe_usage = {} +unwrapped_safe_usage_count = 0 +wrong_wrap_usage = {} +wrong_wrap_usage_count = 0 + +for func_name in loaded_functions: + body = loaded_functions[func_name] + + pattern_call = '\w+' + + for token_match in re.finditer(pattern_call, body): + + token = token_match[0] + + l = max(0, token_match.start() - 40) + r = min(token_match.end() + 150, len(body)) + + line = body[l:r] + + if token in unsafe_variants: + print(f' WARNING: call unsafe variant of function {token} in {func_name}') + unsafe_variant_usage[func_name] = token + unsafe_variant_usage_count += 1 + continue + + if token == 'ereturn' and re.search(r'ereturn\(fcinfo', line): + print(f' WARNING: ereturn cannot be run at fcinfo context in {func_name}, use PG_ERETURN') + + if token == 'return' and func_name in loaded_safe_functions: + m = re.match(r'return\s*([\s\S]*?);', body[token_match.start():r]) + if m[1] != 'true': + print(f' WARNING: in safe function {func_name}: "return" used only with "true", not "{m[1]}"') + + + if token in loaded_functions: + + # if_wrapper_pattern = rf'if \(!{token}\([\s\S]+?, (?:escontext|fcinfo->context)\)\)' + void_wrapper_pattern = rf'(void) {token}\([\s\S]+?, NULL\);' + safe_call_wrapper_pattern = rf'safe_call\({token}, \([\s\S]+?, (?:escontext|fcinfo->context)\)\);' + safe_call_with_free_wrapper_pattern = rf'safe_call_with_free\({token}, \([\s\S]+?, (?:escontext|fcinfo->context)\), \{{[\s\S]+?\}}\);' + pg_safe_call_wrapper_pattern = rf'PG_SAFE_CALL\({token}, \([\s\S]+?, (?:escontext|fcinfo->context)\)\);' + return_wrapper_pattern = rf'return {token}\([\s\S]+?, (?:escontext|fcinfo->context)\);' + direct_call_wrapper_pattern = rf'DirectFunctionCall1Safe\({token}, [\s\S]+?, (?:escontext|fcinfo->context)\);' + + def unite_patterns(l): + return '|'.join(l) + + search_pattern = unite_patterns([ + # if_wrapper_pattern, + safe_call_wrapper_pattern, + safe_call_with_free_wrapper_pattern, + pg_safe_call_wrapper_pattern, + return_wrapper_pattern, + direct_call_wrapper_pattern + ]) + + if not re.search(search_pattern, line): + # print(func_name, token_match, line) + print(f' WARNING: call unwrapped safe function {token} in {func_name}') + unwrapped_safe_usage[func_name] = token + unwrapped_safe_usage_count += 1 + continue + + pg_sc_patterns = unite_patterns([ + pg_safe_call_wrapper_pattern, + direct_call_wrapper_pattern, + ]) + if func_name in loaded_convert_functions and not re.search(pg_sc_patterns, line): + print(f' WARNING: wrong wrapped safe function {token} in PG_FUNCTION {func_name}') + wrong_wrap_usage[func_name] = token + wrong_wrap_usage_count += 1 + continue + + sc_patterns = unite_patterns([ + safe_call_wrapper_pattern, + safe_call_with_free_wrapper_pattern, + return_wrapper_pattern, + ]) + if func_name in loaded_safe_functions and not re.search(sc_patterns, line): + print(f' WARNING: wrong wrapped safe function {token} in SAFE {func_name}') + wrong_wrap_usage[func_name] = token + wrong_wrap_usage_count += 1 + continue + + + +if DEBUG_FLAG: + print(f'FOUND {unsafe_variant_usage_count} UNSAFE VARIANT USAGES') + +if DEBUG_FLAG: + print(f'FOUND {unwrapped_safe_usage_count} UNWRAPPED SAFE FUNCTION USAGES') + +if DEBUG_FLAG: + print(f'FOUND {wrong_wrap_usage_count} WRONG WRAP USAGES') + + +# print(loaded_functions['date_in']) +# print(loaded_functions['timestamptz_interval_bound']) diff --git a/contrib/try_convert/try_convert--1.0.sql b/contrib/try_convert/try_convert--1.0.sql new file mode 100644 index 00000000000..785a66a6f6b --- /dev/null +++ b/contrib/try_convert/try_convert--1.0.sql @@ -0,0 +1,91 @@ +/* contrib/try_convert/try_convert--1.0.sql */ + +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION try_convert" to load this file. \quit + +/* *********************************************** + * try_convert function for PostgreSQL + * *********************************************** */ + +/* generic file access functions */ + +CREATE FUNCTION try_convert(text, anyelement) +RETURNS anyelement +AS 'MODULE_PATHNAME', 'try_convert' +LANGUAGE C; + + +CREATE OR REPLACE FUNCTION add_type_for_try_convert(type regtype) + RETURNS void + LANGUAGE plpgsql AS +$func$ +BEGIN + EXECUTE 'CREATE OR REPLACE FUNCTION try_convert(' || type || ', anyelement) + RETURNS anyelement + AS ''MODULE_PATHNAME'', ''try_convert'' + LANGUAGE C;'; +END +$func$; + +-- NUMBERS +select add_type_for_try_convert('int2'::regtype); +select add_type_for_try_convert('int4'::regtype); +select add_type_for_try_convert('int8'::regtype); +select add_type_for_try_convert('float4'::regtype); +select add_type_for_try_convert('float8'::regtype); +select add_type_for_try_convert('numeric'::regtype); +select add_type_for_try_convert('complex'::regtype); + +-- TIME +select add_type_for_try_convert('date'::regtype); +select add_type_for_try_convert('time'::regtype); +select add_type_for_try_convert('timetz'::regtype); +select add_type_for_try_convert('timestamp'::regtype); +select add_type_for_try_convert('timestamptz'::regtype); +select add_type_for_try_convert('interval'::regtype); + +-- CHARACTER +select add_type_for_try_convert('char'::regtype); +select add_type_for_try_convert('bpchar'::regtype); +select add_type_for_try_convert('varchar'::regtype); +select add_type_for_try_convert('text'::regtype); + +-- BIT STRING +select add_type_for_try_convert('bit'::regtype); +select add_type_for_try_convert('varbit'::regtype); + +select add_type_for_try_convert('bool'::regtype); + +select add_type_for_try_convert('money'::regtype); + +select add_type_for_try_convert('uuid'::regtype); + +-- GEOMETRY +select add_type_for_try_convert('point'::regtype); + +-- IP/MAC +select add_type_for_try_convert('cidr'::regtype); +select add_type_for_try_convert('inet'::regtype); +select add_type_for_try_convert('macaddr'::regtype); + +-- OBJ +select add_type_for_try_convert('json'::regtype); +select add_type_for_try_convert('jsonb'::regtype); +select add_type_for_try_convert('xml'::regtype); diff --git a/contrib/try_convert/try_convert.c b/contrib/try_convert/try_convert.c new file mode 100644 index 00000000000..8c9afccefc3 --- /dev/null +++ b/contrib/try_convert/try_convert.c @@ -0,0 +1,564 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * try_convert.c + * Error-safe type cast function + * + * try_convert(source_value, default_value) casts source_value to the type of + * default_value. Whenever the cast fails because of the data being converted, + * default_value is returned instead of an error being raised. + * + * The conversion to use is looked up the same way the parser does it in + * coerce_type(), see src/backend/parser/parse_coerce.c. + * + * IDENTIFICATION + * contrib/try_convert/try_convert.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_cast.h" +#include "catalog/pg_type.h" +#include "funcapi.h" +#include "nodes/nodeFuncs.h" +#include "parser/parse_coerce.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(try_convert); + +/* + * How a value has to be converted; a subset of the CoercionPathType values + * used by the parser. + */ +typedef enum ConversionType +{ + CONVERSION_TYPE_FUNC, + CONVERSION_TYPE_RELABEL, + CONVERSION_TYPE_VIA_IO, + CONVERSION_TYPE_ARRAY, + CONVERSION_TYPE_NONE +} ConversionType; + +static ConversionType find_conversion_way(Oid targetTypeId, Oid sourceTypeId, + Oid *funcId); +static ConversionType find_typmod_conversion_function(Oid typeId, Oid *funcId); +static void report_conversion_error(MemoryContext oldcontext, bool *is_failed); +static Datum convert_from_function(Datum value, int32 typmod, Oid funcId, + bool *is_failed); +static Datum convert_via_io(Datum value, Oid sourceTypeId, Oid targetTypeId, + bool *is_failed); +static int32 get_call_expr_argtypmod(Node *expr, int argnum); +static int32 get_fn_expr_argtypmod(FmgrInfo *flinfo, int argnum); +static Datum convert(Datum value, ConversionType conversion_type, Oid funcId, + Oid sourceTypeId, Oid targetTypeId, int32 targetTypMod, + bool *is_failed); +static Datum convert_type_typmod(Datum value, int32 sourceTypMod, + Oid targetTypeId, int32 targetTypMod, + bool *is_failed); + + +/* + * Determine how to convert sourceTypeId to targetTypeId, mirroring + * find_coercion_pathway() without the coercion context: try_convert() always + * performs an explicit cast. + * + * Returns CONVERSION_TYPE_NONE if no conversion is possible. *funcId is set + * to the conversion function when one is needed, to InvalidOid otherwise. + */ +static ConversionType +find_conversion_way(Oid targetTypeId, Oid sourceTypeId, Oid *funcId) +{ + ConversionType result = CONVERSION_TYPE_NONE; + HeapTuple tuple; + + *funcId = InvalidOid; + + /* + * Both types have to be known. An invalid type OID means the caller could + * not resolve the argument type, and the lookups below -- TypeCategory() + * in particular -- do not accept one. + */ + if (!OidIsValid(sourceTypeId) || !OidIsValid(targetTypeId)) + return CONVERSION_TYPE_NONE; + + /* Perhaps the types are domains; if so, look at their base types */ + sourceTypeId = getBaseType(sourceTypeId); + targetTypeId = getBaseType(targetTypeId); + + /* Domains are always coercible to and from their base type */ + if (sourceTypeId == targetTypeId) + return CONVERSION_TYPE_RELABEL; + + /* Look in pg_cast */ + tuple = SearchSysCache2(CASTSOURCETARGET, + ObjectIdGetDatum(sourceTypeId), + ObjectIdGetDatum(targetTypeId)); + + if (HeapTupleIsValid(tuple)) + { + Form_pg_cast castForm = (Form_pg_cast) GETSTRUCT(tuple); + + switch (castForm->castmethod) + { + case COERCION_METHOD_FUNCTION: + *funcId = castForm->castfunc; + result = CONVERSION_TYPE_FUNC; + break; + case COERCION_METHOD_INOUT: + result = CONVERSION_TYPE_VIA_IO; + break; + case COERCION_METHOD_BINARY: + result = CONVERSION_TYPE_RELABEL; + break; + default: + elog(ERROR, "unrecognized castmethod: %d", + (int) castForm->castmethod); + break; + } + + ReleaseSysCache(tuple); + } + else + { + /* + * If there's no pg_cast entry, perhaps we are dealing with a pair of + * array types. If so, and if the element types have a suitable cast, + * report that we can coerce with an ArrayCoerceExpr. + * + * Note that the source type can be a domain over array, but not the + * target, because ArrayCoerceExpr won't check domain constraints. + * + * Hack: disallow coercions to oidvector and int2vector, which + * otherwise tend to capture coercions that should go to "real" array + * types. We want those types to be considered "real" arrays for many + * purposes, but not this one. (Also, ArrayCoerceExpr isn't + * guaranteed to produce an output that meets the restrictions of + * these datatypes, such as being 1-dimensional.) + */ + if (targetTypeId != OIDVECTOROID && targetTypeId != INT2VECTOROID) + { + Oid targetElem; + Oid sourceElem; + + if ((targetElem = get_element_type(targetTypeId)) != InvalidOid && + (sourceElem = get_base_element_type(sourceTypeId)) != InvalidOid) + { + ConversionType elempathtype; + Oid elemfuncid; + + elempathtype = find_conversion_way(targetElem, + sourceElem, + &elemfuncid); + if (elempathtype != CONVERSION_TYPE_NONE && + elempathtype != CONVERSION_TYPE_ARRAY) + { + *funcId = elemfuncid; + if (elempathtype == CONVERSION_TYPE_VIA_IO) + result = CONVERSION_TYPE_VIA_IO; + else + result = CONVERSION_TYPE_ARRAY; + } + } + } + + /* + * If we still haven't found a possibility, consider automatic casting + * using I/O functions. We allow assignment casts to string types and + * explicit casts from string types to be handled this way. (The + * CoerceViaIO mechanism is a lot more general than that, but this is + * all we want to allow in the absence of a pg_cast entry.) It would + * probably be better to insist on explicit casts in both directions, + * but this is a compromise to preserve something of the pre-8.3 + * behavior that many types had implicit (yipes!) casts to text. + */ + if (result == CONVERSION_TYPE_NONE) + { + if (TypeCategory(targetTypeId) == TYPCATEGORY_STRING) + result = CONVERSION_TYPE_VIA_IO; + else if (TypeCategory(sourceTypeId) == TYPCATEGORY_STRING) + result = CONVERSION_TYPE_VIA_IO; + } + } + + return result; +} + +/* + * Look up the length coercion function of a type, that is the cast from the + * type to itself, mirroring find_typmod_coercion_function(). + * + * Returns CONVERSION_TYPE_NONE when the type has no length coercion function, + * in which case the value has to be left alone. + */ +static ConversionType +find_typmod_conversion_function(Oid typeId, Oid *funcId) +{ + ConversionType result = CONVERSION_TYPE_NONE; + HeapTuple tuple; + + *funcId = InvalidOid; + + /* Look in pg_cast */ + tuple = SearchSysCache2(CASTSOURCETARGET, + ObjectIdGetDatum(typeId), + ObjectIdGetDatum(typeId)); + + if (HeapTupleIsValid(tuple)) + { + Form_pg_cast castForm = (Form_pg_cast) GETSTRUCT(tuple); + + *funcId = castForm->castfunc; + ReleaseSysCache(tuple); + + /* + * A binary-coercible self-cast carries no function, so there is + * nothing we could call to apply the typmod. + */ + if (OidIsValid(*funcId)) + result = CONVERSION_TYPE_FUNC; + } + + return result; +} + +/* + * Common tail of the PG_CATCH() blocks below. + * + * The conversion functions called by this module report a failure the only way + * they can, by throwing an error, so the only way to keep going is to catch it. + * Ideally we would use the "soft" error handling infrastructure added by + * PostgreSQL 17 (commit ccff2d20ed) instead, which lets an input function + * report a conversion failure without throwing; that requires converting the + * datatype input functions first, so until then we trap the error here. + * + * Trapping an error outside of a subtransaction is only safe as long as the + * called function leaves no global state behind, which holds for the cast and + * type input/output functions we call. Errors that do not come from the data + * being converted must not be swallowed, so query cancellation and assertion + * failures are re-thrown, following what plpgsql does for "EXCEPTION WHEN + * others". + */ +static void +report_conversion_error(MemoryContext oldcontext, bool *is_failed) +{ + int sqlerrcode = geterrcode(); + + if (sqlerrcode == ERRCODE_QUERY_CANCELED || + sqlerrcode == ERRCODE_ASSERT_FAILURE) + PG_RE_THROW(); + + MemoryContextSwitchTo(oldcontext); + FlushErrorState(); + + *is_failed = true; +} + +/* + * Convert a value by calling the cast function funcId. + */ +static Datum +convert_from_function(Datum value, int32 typmod, Oid funcId, bool *is_failed) +{ + MemoryContext oldcontext = CurrentMemoryContext; + volatile Datum res = (Datum) 0; + + PG_TRY(); + { + /* + * Cast functions take either one argument or three, the extra ones + * being the target typmod and the explicit-cast flag. Passing three + * arguments to a one-argument function is harmless, the callee simply + * ignores them. + */ + res = OidFunctionCall3(funcId, + value, + Int32GetDatum(typmod), + BoolGetDatum(true)); + } + PG_CATCH(); + { + report_conversion_error(oldcontext, is_failed); + } + PG_END_TRY(); + + return res; +} + +/* + * Convert a value by running it through the output function of the source type + * and the input function of the target type. + * + * The typmod is not applied here, convert_type_typmod() takes care of it. + */ +static Datum +convert_via_io(Datum value, Oid sourceTypeId, Oid targetTypeId, + bool *is_failed) +{ + FmgrInfo outfunc; + Oid infuncId = InvalidOid; + Oid outfuncId = InvalidOid; + Oid intypioparam = InvalidOid; + bool outtypisvarlena = false; + MemoryContext oldcontext = CurrentMemoryContext; + volatile Datum res = (Datum) 0; + + /* Perhaps the types are domains; if so, look at their base types */ + sourceTypeId = getBaseType(sourceTypeId); + targetTypeId = getBaseType(targetTypeId); + + getTypeOutputInfo(sourceTypeId, &outfuncId, &outtypisvarlena); + fmgr_info(outfuncId, &outfunc); + + getTypeInputInfo(targetTypeId, &infuncId, &intypioparam); + + PG_TRY(); + { + char *string; + + /* the caller has already rejected a NULL input value */ + string = OutputFunctionCall(&outfunc, value); + + res = OidFunctionCall3(infuncId, + CStringGetDatum(string), + ObjectIdGetDatum(intypioparam), + Int32GetDatum(-1)); + + pfree(string); + } + PG_CATCH(); + { + report_conversion_error(oldcontext, is_failed); + } + PG_END_TRY(); + + return res; +} + +/* + * Get the actual typmod of a specific function argument (counting from 0), + * but working from the calling expression tree instead of FmgrInfo. + * + * Returns -1 if information is not available. + */ +static int32 +get_call_expr_argtypmod(Node *expr, int argnum) +{ + List *args; + + if (expr == NULL) + return -1; + + if (IsA(expr, FuncExpr)) + args = ((FuncExpr *) expr)->args; + else if (IsA(expr, OpExpr)) + args = ((OpExpr *) expr)->args; + else if (IsA(expr, DistinctExpr)) + args = ((DistinctExpr *) expr)->args; + else if (IsA(expr, ScalarArrayOpExpr)) + args = ((ScalarArrayOpExpr *) expr)->args; + else if (IsA(expr, ArrayCoerceExpr)) + args = list_make1(((ArrayCoerceExpr *) expr)->arg); + else if (IsA(expr, NullIfExpr)) + args = ((NullIfExpr *) expr)->args; + else if (IsA(expr, WindowFunc)) + args = ((WindowFunc *) expr)->args; + else + return -1; + + if (argnum < 0 || argnum >= list_length(args)) + return -1; + + return exprTypmod((Node *) list_nth(args, argnum)); +} + +/* + * Get the actual typmod of a specific function argument (counting from 0). + * + * Returns -1 if information is not available. + */ +static int32 +get_fn_expr_argtypmod(FmgrInfo *flinfo, int argnum) +{ + /* + * can't return anything useful if we have no FmgrInfo or if its fn_expr + * node has not been initialized + */ + if (!flinfo || !flinfo->fn_expr) + return -1; + + return get_call_expr_argtypmod(flinfo->fn_expr, argnum); +} + +/* + * Apply the conversion found by find_conversion_way() or + * find_typmod_conversion_function(). + * + * A conversion that cannot be performed at all is a query error and is + * reported as such; only failures caused by the data being converted are + * reported through *is_failed. + */ +static Datum +convert(Datum value, ConversionType conversion_type, Oid funcId, + Oid sourceTypeId, Oid targetTypeId, int32 targetTypMod, + bool *is_failed) +{ + switch (conversion_type) + { + case CONVERSION_TYPE_RELABEL: + return value; + + case CONVERSION_TYPE_FUNC: + return convert_from_function(value, targetTypMod, funcId, + is_failed); + + case CONVERSION_TYPE_VIA_IO: + return convert_via_io(value, sourceTypeId, targetTypeId, + is_failed); + + case CONVERSION_TYPE_ARRAY: + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("try_convert() does not support casts between array types"), + errdetail("Cannot cast type %s to %s.", + format_type_be(sourceTypeId), + format_type_be(targetTypeId)))); + break; + + case CONVERSION_TYPE_NONE: + ereport(ERROR, + (errcode(ERRCODE_CANNOT_COERCE), + errmsg("cannot cast type %s to %s", + format_type_be(sourceTypeId), + format_type_be(targetTypeId)))); + break; + } + + elog(ERROR, "unrecognized conversion method: %d", (int) conversion_type); + return (Datum) 0; /* keep compiler quiet */ +} + +/* + * Coerce a value of targetTypeId to targetTypMod. + */ +static Datum +convert_type_typmod(Datum value, int32 sourceTypMod, Oid targetTypeId, + int32 targetTypMod, bool *is_failed) +{ + ConversionType conversion_type; + Oid funcId; + + if (targetTypMod < 0 || targetTypMod == sourceTypMod) + return value; + + conversion_type = find_typmod_conversion_function(targetTypeId, &funcId); + + /* + * If the target type has no length coercion function, just leave the value + * alone, the same way coerce_type_typmod() does. + */ + if (conversion_type == CONVERSION_TYPE_NONE) + return value; + + return convert(value, conversion_type, funcId, targetTypeId, targetTypeId, + targetTypMod, is_failed); +} + +/* + * try_convert(source_value, default_value) -> converted value or default_value + * + * The target type is taken from the second argument, which is also the value + * returned when the conversion fails. + */ +Datum +try_convert(PG_FUNCTION_ARGS) +{ + Oid sourceTypeId; + int32 sourceTypMod; + Oid targetTypeId; + int32 targetTypMod; + Oid baseTypeId; + int32 baseTypMod; + Oid funcId; + ConversionType conversion_type; + Datum value; + Datum res; + int32 resTypMod; + bool is_failed = false; + + /* A NULL input converts to NULL, whatever the default value is */ + if (PG_ARGISNULL(0)) + PG_RETURN_NULL(); + + sourceTypeId = get_fn_expr_argtype(fcinfo->flinfo, 0); + sourceTypMod = get_fn_expr_argtypmod(fcinfo->flinfo, 0); + + targetTypeId = get_fn_expr_argtype(fcinfo->flinfo, 1); + targetTypMod = get_fn_expr_argtypmod(fcinfo->flinfo, 1); + + if (!OidIsValid(sourceTypeId) || !OidIsValid(targetTypeId)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not determine the argument types of try_convert()"))); + + baseTypMod = targetTypMod; + baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypMod); + + if (targetTypeId != baseTypeId) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("try_convert() does not support casts to domain types"), + errdetail("Cannot cast type %s to %s.", + format_type_be(sourceTypeId), + format_type_be(targetTypeId)))); + + value = PG_GETARG_DATUM(0); + + conversion_type = find_conversion_way(targetTypeId, sourceTypeId, &funcId); + + if (conversion_type == CONVERSION_TYPE_RELABEL) + { + res = value; + resTypMod = sourceTypMod; + } + else + { + res = convert(value, conversion_type, funcId, sourceTypeId, baseTypeId, + baseTypMod, &is_failed); + resTypMod = -1; + } + + if (!is_failed) + res = convert_type_typmod(res, resTypMod, targetTypeId, targetTypMod, + &is_failed); + + if (is_failed) + { + /* the value could not be converted, fall back to the default one */ + fcinfo->isnull = PG_ARGISNULL(1); + return PG_GETARG_DATUM(1); + } + + return res; +} diff --git a/contrib/try_convert/try_convert.control b/contrib/try_convert/try_convert.control new file mode 100644 index 00000000000..022ba632784 --- /dev/null +++ b/contrib/try_convert/try_convert.control @@ -0,0 +1,6 @@ +# try_convert extension +comment = 'function for type cast' +default_version = '1.0' +module_pathname = '$libdir/try_convert' +relocatable = true +trusted = true