What is passing the target type into the FOREACH iterator actually worth?
=========================================================================

Setup
-----

master (1c9c3589042) + v20260708-0001-FOREACH-scalar-IN-ARRAY-jsonb_expr.patch,
plus the instrumentation diff below.  Built with meson, buildtype=debugoptimized
(-O2 -g), -Dcassert=false.  gcc 15.2, aarch64, fsync=off, shared_buffers=256MB.
Times are the minimum of 10 runs; differences below ~5% are run-to-run noise.

The instrumentation adds a PG_FOREACH_BENCH_MODE environment variable that
selects among four designs, so that one build covers all of them:

  0  as submitted -- expected_typid is passed to the iterator constructor.

  1  "naive drop" -- the target type is forced to InvalidOid.  In the submitted
     code that makes the constructor fall back to expected_typid = JSONBOID,
     i.e. every element is materialized as a jsonb and the caller casts from
     jsonb.

  2  "natural types" -- no target type at all; scalars are returned as their
     natural SQL types (text/numeric/bool) and only objects/arrays are
     materialized as jsonb.

  3  "split" -- like 2, but the iterator additionally offers a coerce() method
     which the caller invokes once it knows its own target type.  This is the
     "separate iterate from convert" design.


Results
-------

min ms, 100k-element arrays (50k for the object cases)

  case                                        mode 0   mode 1   mode 2   mode 3
  ------------------------------------------------------------------------------
  f_int        int target, scalar elems         9.82    17.20    10.20    10.19
  f_num        numeric target, scalar elems    10.79    21.87    11.55    11.95
  f_text       text target, scalar elems       11.21    19.47    10.60    11.04
  f_jsonb      jsonb target, scalar elems      10.61    10.57    18.21    18.36
  f_jsonb_obj  jsonb target, object elems       8.23     8.27     8.21     8.35
  f_comp       composite target, object elems  16.82    ERROR    ERROR     18.27
  ------------------------------------------------------------------------------
  f_arr_int    control, pre-existing array      4.74     4.52     4.82     4.96
  f_sql_int    control, FOR IN SELECT          21.03    20.70    20.63    21.40

The ERROR in modes 1 and 2 is:

  ERROR:  cannot assign non-composite value to a record variable

Mode 1 also silently changes the text result: f_text returns 988895 instead of
788895, because each JSON string is materialized as a jsonb and then cast to
text, which keeps the JSON quoting.


Reading of the numbers
----------------------

1. For scalar targets the target type is worth nothing.  Mode 2 matches mode 0
   on int, numeric and text targets.  This is what the code says too:
   JsonbValueToDatum() never looks at expected_typid on the jbvNumeric,
   jbvString or jbvBool paths -- it returns numeric/text/bool and leaves the
   conversion to exec_assign_value().

2. The 75-100% regression in mode 1 is an artifact of the constructor's
   fallback (InvalidOid -> JSONBOID), not evidence that the target type is
   useful.  Mode 2 carries exactly the same information and costs nothing.

3. For composite targets it is worth 5-10%, which is close to noise.  Mode 3
   makes the identical json_populate_type() call with the identical cache; the
   only difference is that the caller invokes it after the fact.

4. The one place the target type is genuinely load-bearing is a json/jsonb
   target over scalar elements, and there the argument is correctness, not
   speed.  Running the patch's own plpgsql_foreach test under mode 3 fails on
   exactly two cases:

     -NOTICE:  null          +NOTICE:  <NULL>
     -NOTICE:  "Hi"          +ERROR:  invalid input syntax for type json
                              DETAIL:  Token "Hi" is invalid.

   Decomposing a scalar JsonbValue is lossy: afterwards you cannot tell SQL
   NULL from JSON null, and a jbvString has lost its JSON quoting.  The
   iterator has to know before it decomposes.  Every other test in the suite
   passes under mode 3, byte-identical.

   Note the scope: this costs 1.7x only for scalar elements assigned to a jsonb
   variable.  For the common shape -- FOREACH o jsonb IN ARRAY <array of
   objects> -- all four modes are identical (8.2 vs 8.4), because container
   elements are returned as jsonb either way.

5. Incidentally, the headline speedup here is 2.1x over FOR IN SELECT
   jsonb_array_elements (21.03 -> 9.82), not the 3-4x claimed upthread; my
   comparison query casts with e::int, so the exact formulation matters.  And
   the jsonb path remains 2.1x slower than the pre-existing array path.


Suggested conclusion
--------------------

The Oid expected_typid / int32 expected_typmod pair is doing exactly one job:
telling the iterator "do not decompose, hand me elements in your own container
type".  That is a boolean, not an Oid.  A more type-independent API would be

  - a bool in the constructor -- every container type can define what its own
    native element form is (jsonb -> jsonb, hstore -> text, array -> element
    type) -- plus

  - the optional coerce() callback for whatever the type can convert better
    than the caller's cast machinery (jsonb -> composite via
    json_populate_type), which measures free.

That removes arbitrary target-type reasoning from every iterator, keeps the
json/jsonb semantics correct, keeps composite population fast, and gives a
natural home for returning error codes instead of ereport'ing caller-specific
messages.  It also drops expected_typmod, which nothing currently uses.
