<programlisting>
 CREATE VIEW vista AS SELECT 'Hello World';
 </programlisting>
-    is bad form in two ways: the column name defaults to <literal>?column?</>,
-    and the column data type defaults to <type>unknown</>.  If you want a
-    string literal in a view's result, use something like:
+    is bad form because the column name defaults to <literal>?column?</>;
+    also, the column data type defaults to <type>text</>, which might not
+    be what you wanted.  Better style for a string literal in a view's
+    result is something like:
 <programlisting>
 CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
 </programlisting>
 
 <para>
 If all inputs are of type <type>unknown</type>, resolve as type
 <type>text</type> (the preferred type of the string category).
-Otherwise, <type>unknown</type> inputs are ignored.
+Otherwise, <type>unknown</type> inputs are ignored for the purposes
+of the remaining rules.
 </para>
 </step>
 
 result type is resolved as <type>real</>.
 </para>
 </example>
+</sect1>
+
+<sect1 id="typeconv-select">
+<title><literal>SELECT</literal> Output Columns</title>
+
+<indexterm zone="typeconv-select">
+ <primary>SELECT</primary>
+ <secondary>determination of result type</secondary>
+</indexterm>
+
+<para>
+The rules given in the preceding sections will result in assignment
+of non-<type>unknown</> data types to all expressions in a SQL query,
+except for unspecified-type literals that appear as simple output
+columns of a <command>SELECT</> command.  For example, in
+
+<screen>
+SELECT 'Hello World';
+</screen>
+
+there is nothing to identify what type the string literal should be
+taken as.  In this situation <productname>PostgreSQL</> will fall back
+to resolving the literal's type as <type>text</>.
+</para>
+
+<para>
+When the <command>SELECT</> is one arm of a <literal>UNION</>
+(or <literal>INTERSECT</> or <literal>EXCEPT</>) construct, or when it
+appears within <command>INSERT ... SELECT</>, this rule is not applied
+since rules given in preceding sections take precedence.  The type of an
+unspecified-type literal can be taken from the other <literal>UNION</> arm
+in the first case, or from the destination column in the second case.
+</para>
+
+<para>
+<literal>RETURNING</> lists are treated the same as <command>SELECT</>
+output lists for this purpose.
+</para>
+
+<note>
+ <para>
+  Prior to <productname>PostgreSQL</> 10, this rule did not exist, and
+  unspecified-type literals in a <command>SELECT</> output list were
+  left as type <type>unknown</>.  That had assorted bad consequences,
+  so it's been changed.
+ </para>
+</note>
 
 </sect1>
 </chapter>
 
    char        att_typtype = get_typtype(atttypid);
    Oid         att_typelem;
 
-   if (atttypid == UNKNOWNOID)
-   {
-       /*
-        * Warn user, but don't fail, if column to be created has UNKNOWN type
-        * (usually as a result of a 'retrieve into' - jolly)
-        */
-       ereport(WARNING,
-               (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-                errmsg("column \"%s\" has type %s", attname, "unknown"),
-                errdetail("Proceeding with relation creation anyway.")));
-   }
-   else if (att_typtype == TYPTYPE_PSEUDO)
+   if (atttypid == UNKNOWNOID ||
+       att_typtype == TYPTYPE_PSEUDO)
    {
        /*
         * Refuse any attempt to create a pseudo-type column, except for a
 
 Query *
 parse_sub_analyze(Node *parseTree, ParseState *parentParseState,
                  CommonTableExpr *parentCTE,
-                 bool locked_from_parent)
+                 bool locked_from_parent,
+                 bool resolve_unknowns)
 {
    ParseState *pstate = make_parsestate(parentParseState);
    Query      *query;
 
    pstate->p_parent_cte = parentCTE;
    pstate->p_locked_from_parent = locked_from_parent;
+   pstate->p_resolve_unknowns = resolve_unknowns;
 
    query = transformStmt(pstate, parseTree);
 
         * otherwise the behavior of SELECT within INSERT might be different
         * from a stand-alone SELECT. (Indeed, Postgres up through 6.5 had
         * bugs of just that nature...)
+        *
+        * The sole exception is that we prevent resolving unknown-type
+        * outputs as TEXT.  This does not change the semantics since if the
+        * column type matters semantically, it would have been resolved to
+        * something else anyway.  Doing this lets us resolve such outputs as
+        * the target column's type, which we handle below.
         */
        sub_pstate->p_rtable = sub_rtable;
        sub_pstate->p_joinexprs = NIL;  /* sub_rtable has no joins */
        sub_pstate->p_namespace = sub_namespace;
+       sub_pstate->p_resolve_unknowns = false;
 
        selectQuery = transformStmt(sub_pstate, stmt->selectStmt);
 
                                                   pstate->p_windowdefs,
                                                   &qry->targetList);
 
+   /* resolve any still-unresolved output columns as being type text */
+   if (pstate->p_resolve_unknowns)
+       resolveTargetListUnknowns(pstate, qry->targetList);
+
    qry->rtable = pstate->p_rtable;
    qry->jointree = makeFromExpr(pstate->p_joinlist, qual);
 
        /*
         * Transform SelectStmt into a Query.
         *
+        * This works the same as SELECT transformation normally would, except
+        * that we prevent resolving unknown-type outputs as TEXT.  This does
+        * not change the subquery's semantics since if the column type
+        * matters semantically, it would have been resolved to something else
+        * anyway.  Doing this lets us resolve such outputs using
+        * select_common_type(), below.
+        *
         * Note: previously transformed sub-queries don't affect the parsing
         * of this sub-query, because they are not in the toplevel pstate's
         * namespace list.
         */
-       selectQuery = parse_sub_analyze((Node *) stmt, pstate, NULL, false);
+       selectQuery = parse_sub_analyze((Node *) stmt, pstate,
+                                       NULL, false, false);
 
        /*
         * Check for bogus references to Vars on the current query level (but
    /* mark column origins */
    markTargetListOrigins(pstate, rlist);
 
+   /* resolve any still-unresolved output columns as being type text */
+   if (pstate->p_resolve_unknowns)
+       resolveTargetListUnknowns(pstate, rlist);
+
    /* restore state */
    pstate->p_next_resno = save_next_resno;
 
 
     * Analyze and transform the subquery.
     */
    query = parse_sub_analyze(r->subquery, pstate, NULL,
-                             isLockedRefname(pstate, r->alias->aliasname));
+                             isLockedRefname(pstate, r->alias->aliasname),
+                             true);
 
    /* Restore state */
    pstate->p_lateral_active = false;
 
    /* Analysis not done already */
    Assert(!IsA(cte->ctequery, Query));
 
-   query = parse_sub_analyze(cte->ctequery, pstate, cte, false);
+   query = parse_sub_analyze(cte->ctequery, pstate, cte, false, true);
    cte->ctequery = (Node *) query;
 
    /*
 
        /*
         * If the CTE is recursive, force the exposed column type of any
-        * "unknown" column to "text".  This corresponds to the fact that
-        * SELECT 'foo' UNION SELECT 'bar' will ultimately produce text. We
-        * might see "unknown" as a result of an untyped literal in the
-        * non-recursive term's select list, and if we don't convert to text
-        * then we'll have a mismatch against the UNION result.
+        * "unknown" column to "text".  We must deal with this here because
+        * we're called on the non-recursive term before there's been any
+        * attempt to force unknown output columns to some other type.  We
+        * have to resolve unknowns before looking at the recursive term.
         *
         * The column might contain 'foo' COLLATE "bar", so don't override
         * collation if it's already set.
 
    /*
     * OK, let's transform the sub-SELECT.
     */
-   qtree = parse_sub_analyze(sublink->subselect, pstate, NULL, false);
+   qtree = parse_sub_analyze(sublink->subselect, pstate, NULL, false, true);
 
    /*
     * Check that we got a SELECT.  Anything else should be impossible given
 
 
    /* Fill in fields that don't start at null/false/zero */
    pstate->p_next_resno = 1;
+   pstate->p_resolve_unknowns = true;
 
    if (parentParseState)
    {
 
 }
 
 
+/*
+ * resolveTargetListUnknowns()
+ *     Convert any unknown-type targetlist entries to type TEXT.
+ *
+ * We do this after we've exhausted all other ways of identifying the output
+ * column types of a query.
+ */
+void
+resolveTargetListUnknowns(ParseState *pstate, List *targetlist)
+{
+   ListCell   *l;
+
+   foreach(l, targetlist)
+   {
+       TargetEntry *tle = (TargetEntry *) lfirst(l);
+       Oid         restype = exprType((Node *) tle->expr);
+
+       if (restype == UNKNOWNOID)
+       {
+           tle->expr = (Expr *) coerce_type(pstate, (Node *) tle->expr,
+                                            restype, TEXTOID, -1,
+                                            COERCION_IMPLICIT,
+                                            COERCE_IMPLICIT_CAST,
+                                            -1);
+       }
+   }
+}
+
+
 /*
  * markTargetListOrigins()
  *     Mark targetlist columns that are simple Vars with the source
  *     table's OID and column number.
  *
- * Currently, this is done only for SELECT targetlists, since we only
- * need the info if we are going to send it to the frontend.
+ * Currently, this is done only for SELECT targetlists and RETURNING lists,
+ * since we only need the info if we are going to send it to the frontend.
  */
 void
 markTargetListOrigins(ParseState *pstate, List *targetlist)
 
    check_for_reg_data_type_usage(&old_cluster);
    check_for_isn_and_int8_passing_mismatch(&old_cluster);
 
+   /* Pre-PG 10 allowed tables with 'unknown' type columns */
+   if (GET_MAJOR_VERSION(old_cluster.major_version) <= 906)
+       old_9_6_check_for_unknown_data_type_usage(&old_cluster);
+
    /* 9.5 and below should not have roles starting with pg_ */
    if (GET_MAJOR_VERSION(old_cluster.major_version) <= 905)
        check_for_pg_role_prefix(&old_cluster);
 
 void new_9_0_populate_pg_largeobject_metadata(ClusterInfo *cluster,
                                         bool check_mode);
 void       old_9_3_check_for_line_data_type_usage(ClusterInfo *cluster);
+void       old_9_6_check_for_unknown_data_type_usage(ClusterInfo *cluster);
 
 /* parallel.c */
 void parallel_exec_prog(const char *log_file, const char *opt_log_file,
 
    else
        check_ok();
 }
+
+
+/*
+ * old_9_6_check_for_unknown_data_type_usage()
+ * 9.6 -> 10
+ * It's no longer allowed to create tables or views with "unknown"-type
+ * columns.  We do not complain about views with such columns, because
+ * they should get silently converted to "text" columns during the DDL
+ * dump and reload; it seems unlikely to be worth making users do that
+ * by hand.  However, if there's a table with such a column, the DDL
+ * reload will fail, so we should pre-detect that rather than failing
+ * mid-upgrade.  Worse, if there's a matview with such a column, the
+ * DDL reload will silently change it to "text" which won't match the
+ * on-disk storage (which is like "cstring").  So we *must* reject that.
+ * Also check composite types, in case they are used for table columns.
+ * We needn't check indexes, because "unknown" has no opclasses.
+ */
+void
+old_9_6_check_for_unknown_data_type_usage(ClusterInfo *cluster)
+{
+   int         dbnum;
+   FILE       *script = NULL;
+   bool        found = false;
+   char        output_path[MAXPGPATH];
+
+   prep_status("Checking for invalid \"unknown\" user columns");
+
+   snprintf(output_path, sizeof(output_path), "tables_using_unknown.txt");
+
+   for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+   {
+       PGresult   *res;
+       bool        db_used = false;
+       int         ntups;
+       int         rowno;
+       int         i_nspname,
+                   i_relname,
+                   i_attname;
+       DbInfo     *active_db = &cluster->dbarr.dbs[dbnum];
+       PGconn     *conn = connectToServer(cluster, active_db->db_name);
+
+       res = executeQueryOrDie(conn,
+                               "SELECT n.nspname, c.relname, a.attname "
+                               "FROM   pg_catalog.pg_class c, "
+                               "       pg_catalog.pg_namespace n, "
+                               "       pg_catalog.pg_attribute a "
+                               "WHERE  c.oid = a.attrelid AND "
+                               "       NOT a.attisdropped AND "
+                               "       a.atttypid = 'pg_catalog.unknown'::pg_catalog.regtype AND "
+                          "        c.relkind IN ('r', 'c', 'm') AND "
+                               "       c.relnamespace = n.oid AND "
+       /* exclude possible orphaned temp tables */
+                               "       n.nspname !~ '^pg_temp_' AND "
+                        "      n.nspname !~ '^pg_toast_temp_' AND "
+                               "       n.nspname NOT IN ('pg_catalog', 'information_schema')");
+
+       ntups = PQntuples(res);
+       i_nspname = PQfnumber(res, "nspname");
+       i_relname = PQfnumber(res, "relname");
+       i_attname = PQfnumber(res, "attname");
+       for (rowno = 0; rowno < ntups; rowno++)
+       {
+           found = true;
+           if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
+               pg_fatal("could not open file \"%s\": %s\n", output_path,
+                        strerror(errno));
+           if (!db_used)
+           {
+               fprintf(script, "Database: %s\n", active_db->db_name);
+               db_used = true;
+           }
+           fprintf(script, "  %s.%s.%s\n",
+                   PQgetvalue(res, rowno, i_nspname),
+                   PQgetvalue(res, rowno, i_relname),
+                   PQgetvalue(res, rowno, i_attname));
+       }
+
+       PQclear(res);
+
+       PQfinish(conn);
+   }
+
+   if (script)
+       fclose(script);
+
+   if (found)
+   {
+       pg_log(PG_REPORT, "fatal\n");
+       pg_fatal("Your installation contains the \"unknown\" data type in user tables.  This\n"
+                "data type is no longer allowed in tables, so this cluster cannot currently\n"
+                "be upgraded.  You can remove the problem tables and restart the upgrade.\n"
+                "A list of the problem columns is in the file:\n"
+                "    %s\n\n", output_path);
+   }
+   else
+       check_ok();
+}
 
 
 extern Query *parse_sub_analyze(Node *parseTree, ParseState *parentParseState,
                  CommonTableExpr *parentCTE,
-                 bool locked_from_parent);
+                 bool locked_from_parent,
+                 bool resolve_unknowns);
 
 extern Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree);
 extern Query *transformStmt(ParseState *pstate, Node *parseTree);
 
  * p_locked_from_parent: true if parent query level applies FOR UPDATE/SHARE
  * to this subquery as a whole.
  *
+ * p_resolve_unknowns: resolve unknown-type SELECT output columns as type TEXT
+ * (this is true by default).
+ *
  * p_hasAggs, p_hasWindowFuncs, etc: true if we've found any of the indicated
  * constructs in the query.
  *
    List       *p_locking_clause;       /* raw FOR UPDATE/FOR SHARE info */
    bool        p_locked_from_parent;   /* parent has marked this subquery
                                         * with FOR UPDATE/FOR SHARE */
+   bool        p_resolve_unknowns;     /* resolve unknown-type SELECT outputs
+                                        * as type text */
 
    /* Flags telling about things found in the query: */
    bool        p_hasAggs;
 
                    ParseExprKind exprKind);
 extern List *transformExpressionList(ParseState *pstate, List *exprlist,
                        ParseExprKind exprKind, bool allowDefault);
+extern void resolveTargetListUnknowns(ParseState *pstate, List *targetlist);
 extern void markTargetListOrigins(ParseState *pstate, List *targetlist);
 extern TargetEntry *transformTargetEntry(ParseState *pstate,
                     Node *node, Node *expr, ParseExprKind exprKind,
 
 CREATE TABLE testjsonb (
        j jsonb
 );
+CREATE TABLE unknowntab (
+   u unknown    -- fail
+);
+ERROR:  column "u" has pseudo-type unknown
+CREATE TYPE unknown_comptype AS (
+   u unknown    -- fail
+);
+ERROR:  column "u" has pseudo-type unknown
 CREATE TABLE IF NOT EXISTS test_tsvector(
    t text,
    a tsvector
 
  mysecview4 | v       | {security_barrier=false}
 (4 rows)
 
+-- Check that unknown literals are converted to "text" in CREATE VIEW,
+-- so that we don't end up with unknown-type columns.
+CREATE VIEW unspecified_types AS
+  SELECT 42 as i, 42.5 as num, 'foo' as u, 'foo'::unknown as u2, null as n;
+\d+ unspecified_types
+                   View "testviewschm2.unspecified_types"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Description 
+--------+---------+-----------+----------+---------+----------+-------------
+ i      | integer |           |          |         | plain    | 
+ num    | numeric |           |          |         | main     | 
+ u      | text    |           |          |         | extended | 
+ u2     | text    |           |          |         | extended | 
+ n      | text    |           |          |         | extended | 
+View definition:
+ SELECT 42 AS i,
+    42.5 AS num,
+    'foo'::text AS u,
+    'foo'::text AS u2,
+    NULL::text AS n;
+
+SELECT * FROM unspecified_types;
+ i  | num  |  u  | u2  | n 
+----+------+-----+-----+---
+ 42 | 42.5 | foo | foo | 
+(1 row)
+
 -- This test checks that proper typmods are assigned in a multi-row VALUES
 CREATE VIEW tt1 AS
   SELECT * FROM (
 
 drop cascades to materialized view mvtest_mv_v_2
 drop cascades to materialized view mvtest_mv_v_3
 drop cascades to materialized view mvtest_mv_v_4
+-- Check that unknown literals are converted to "text" in CREATE MATVIEW,
+-- so that we don't end up with unknown-type columns.
+CREATE MATERIALIZED VIEW mv_unspecified_types AS
+  SELECT 42 as i, 42.5 as num, 'foo' as u, 'foo'::unknown as u2, null as n;
+\d+ mv_unspecified_types
+                      Materialized view "public.mv_unspecified_types"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ i      | integer |           |          |         | plain    |              | 
+ num    | numeric |           |          |         | main     |              | 
+ u      | text    |           |          |         | extended |              | 
+ u2     | text    |           |          |         | extended |              | 
+ n      | text    |           |          |         | extended |              | 
+View definition:
+ SELECT 42 AS i,
+    42.5 AS num,
+    'foo'::text AS u,
+    'foo'::text AS u2,
+    NULL::text AS n;
+
+SELECT * FROM mv_unspecified_types;
+ i  | num  |  u  | u2  | n 
+----+------+-----+-----+---
+ 42 | 42.5 | foo | foo | 
+(1 row)
+
+DROP MATERIALIZED VIEW mv_unspecified_types;
 -- make sure that create WITH NO DATA does not plan the query (bug #13907)
 create materialized view mvtest_error as select 1/0 as x;  -- fail
 ERROR:  division by zero
 
       |                3
 (5 rows)
 
+-- Unspecified-type literals in output columns should resolve as text
+SELECT *, pg_typeof(f1) FROM
+  (SELECT 'foo' AS f1 FROM generate_series(1,3)) ss ORDER BY 1;
+ f1  | pg_typeof 
+-----+-----------
+ foo | text
+ foo | text
+ foo | text
+(3 rows)
+
+-- ... unless there's context to suggest differently
+explain verbose select '42' union all select '43';
+                   QUERY PLAN                    
+-------------------------------------------------
+ Append  (cost=0.00..0.04 rows=2 width=32)
+   ->  Result  (cost=0.00..0.01 rows=1 width=32)
+         Output: '42'::text
+   ->  Result  (cost=0.00..0.01 rows=1 width=32)
+         Output: '43'::text
+(5 rows)
+
+explain verbose select '42' union all select 43;
+                   QUERY PLAN                   
+------------------------------------------------
+ Append  (cost=0.00..0.04 rows=2 width=4)
+   ->  Result  (cost=0.00..0.01 rows=1 width=4)
+         Output: 42
+   ->  Result  (cost=0.00..0.01 rows=1 width=4)
+         Output: 43
+(5 rows)
+
 --
 -- Use some existing tables in the regression test
 --
 
 
 -- Test behavior with an unknown-type literal in the WITH
 WITH q AS (SELECT 'foo' AS x)
-SELECT x, x IS OF (unknown) as is_unknown FROM q;
-  x  | is_unknown 
------+------------
+SELECT x, x IS OF (text) AS is_text FROM q;
+  x  | is_text 
+-----+---------
  foo | t
 (1 row)
 
 UNION ALL
     SELECT n || ' bar' FROM t WHERE length(n) < 20
 )
-SELECT n, n IS OF (text) as is_text FROM t;
+SELECT n, n IS OF (text) AS is_text FROM t;
             n            | is_text 
 -------------------------+---------
  foo                     | t
  foo bar bar bar bar bar | t
 (6 rows)
 
+-- In a perfect world, this would work and resolve the literal as int ...
+-- but for now, we have to be content with resolving to text too soon.
+WITH RECURSIVE t(n) AS (
+    SELECT '7'
+UNION ALL
+    SELECT n+1 FROM t WHERE n < 10
+)
+SELECT n, n IS OF (int) AS is_int FROM t;
+ERROR:  operator does not exist: text + integer
+LINE 4:     SELECT n+1 FROM t WHERE n < 10
+                    ^
+HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.
 --
 -- Some examples with a tree
 --
 
 CREATE FUNCTION test1 (int) RETURNS int LANGUAGE SQL
     AS 'SELECT ''not an integer'';';
 ERROR:  return type mismatch in function declared to return integer
-DETAIL:  Actual return type is unknown.
+DETAIL:  Actual return type is text.
 CONTEXT:  SQL function "test1"
 CREATE FUNCTION test1 (int) RETURNS int LANGUAGE SQL
     AS 'not even SQL';
 
        j jsonb
 );
 
+CREATE TABLE unknowntab (
+   u unknown    -- fail
+);
+
+CREATE TYPE unknown_comptype AS (
+   u unknown    -- fail
+);
+
 CREATE TABLE IF NOT EXISTS test_tsvector(
    t text,
    a tsvector
 
                      'mysecview3'::regclass, 'mysecview4'::regclass)
        ORDER BY relname;
 
+-- Check that unknown literals are converted to "text" in CREATE VIEW,
+-- so that we don't end up with unknown-type columns.
+
+CREATE VIEW unspecified_types AS
+  SELECT 42 as i, 42.5 as num, 'foo' as u, 'foo'::unknown as u2, null as n;
+\d+ unspecified_types
+SELECT * FROM unspecified_types;
+
 -- This test checks that proper typmods are assigned in a multi-row VALUES
 
 CREATE VIEW tt1 AS
 
 SELECT * FROM mvtest_mv_v_4;
 DROP TABLE mvtest_v CASCADE;
 
+-- Check that unknown literals are converted to "text" in CREATE MATVIEW,
+-- so that we don't end up with unknown-type columns.
+CREATE MATERIALIZED VIEW mv_unspecified_types AS
+  SELECT 42 as i, 42.5 as num, 'foo' as u, 'foo'::unknown as u2, null as n;
+\d+ mv_unspecified_types
+SELECT * FROM mv_unspecified_types;
+DROP MATERIALIZED VIEW mv_unspecified_types;
+
 -- make sure that create WITH NO DATA does not plan the query (bug #13907)
 create materialized view mvtest_error as select 1/0 as x;  -- fail
 create materialized view mvtest_error as select 1/0 as x with no data;
 
   WHERE (f1, f2) IN (SELECT f2, CAST(f3 AS int4) FROM SUBSELECT_TBL
                      WHERE f3 IS NOT NULL);
 
+-- Unspecified-type literals in output columns should resolve as text
+
+SELECT *, pg_typeof(f1) FROM
+  (SELECT 'foo' AS f1 FROM generate_series(1,3)) ss ORDER BY 1;
+
+-- ... unless there's context to suggest differently
+
+explain verbose select '42' union all select '43';
+explain verbose select '42' union all select 43;
+
 --
 -- Use some existing tables in the regression test
 --
 
 
 -- Test behavior with an unknown-type literal in the WITH
 WITH q AS (SELECT 'foo' AS x)
-SELECT x, x IS OF (unknown) as is_unknown FROM q;
+SELECT x, x IS OF (text) AS is_text FROM q;
 
 WITH RECURSIVE t(n) AS (
     SELECT 'foo'
 UNION ALL
     SELECT n || ' bar' FROM t WHERE length(n) < 20
 )
-SELECT n, n IS OF (text) as is_text FROM t;
+SELECT n, n IS OF (text) AS is_text FROM t;
+
+-- In a perfect world, this would work and resolve the literal as int ...
+-- but for now, we have to be content with resolving to text too soon.
+WITH RECURSIVE t(n) AS (
+    SELECT '7'
+UNION ALL
+    SELECT n+1 FROM t WHERE n < 10
+)
+SELECT n, n IS OF (int) AS is_int FROM t;
 
 --
 -- Some examples with a tree