/*-------------------------------------------------------------------------
 *
 * parse_name.c
 *
 *			  parse function signature
 *			  parse identifier, and type name
 *
 * by Pavel Stehule 2013-2022
 *
 *-------------------------------------------------------------------------
 */

#include "plpgsql_check.h"

#include <string.h>

#include "catalog/namespace.h"
#include "parser/scansup.h"
#include "parser/parse_type.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/regproc.h"

#if PG_VERSION_NUM < 110000

#include "utils/typcache.h"

#endif

/*
 * Originaly this structure was named TokenType, but this is in collision
 * with name from NT SDK. So it is renamed to PragmaTokenType.
 */
typedef struct
{
	int		value;
	const char *substr;
	size_t		size;
} PragmaTokenType;

typedef struct
{
	const char *str;
	PragmaTokenType	saved_token;
	bool		saved_token_is_valid;
} TokenizerState;

static char *make_string(PragmaTokenType *token);

static const char *tagstr = "@plpgsql_check_options:";

#define		PRAGMA_TOKEN_IDENTIF		128
#define		PRAGMA_TOKEN_QIDENTIF		129
#define		PRAGMA_TOKEN_NUMBER			130
#define		PRAGMA_TOKEN_STRING			131

#ifdef _MSC_VER

static void *
memmem(const void *haystack, size_t haystack_len,
	   const void * const needle, const size_t needle_len)
{
	if (haystack == NULL) return NULL; // or assert(haystack != NULL);
	if (haystack_len == 0) return NULL;
	if (needle == NULL) return NULL; // or assert(needle != NULL);
	if (needle_len == 0) return NULL;

	for (const char *h = haystack;
		 haystack_len >= needle_len;
		 ++h, --haystack_len)
	{
		if (!memcmp(h, needle, needle_len))
		{
			return (void *) h;
		}
	}
	return NULL;
}

#endif

/*
 * Is character a valid identifier start?
 * Must match scan.l's {ident_start} character class.
 */
static bool
is_ident_start(unsigned char c)
{
	/* Underscores and ASCII letters are OK */
	if (c == '_')
		return true;
	if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
		return true;
	/* Any high-bit-set character is OK (might be part of a multibyte char) */
	if (IS_HIGHBIT_SET(c))
		return true;
	return false;
}

/*
 * Is character a valid identifier continuation?
 * Must match scan.l's {ident_cont} character class.
 */
static bool
is_ident_cont(unsigned char c)
{
	/* Can be digit or dollar sign ... */
	if ((c >= '0' && c <= '9') || c == '$')
		return true;
	/* ... or an identifier start character */
	return is_ident_start(c);
}


/*
 * parse_ident - returns list of Strings when input is valid name.
 * Returns NIL, when input string is signature. Can raise a error,
 * when input is not valid identifier.
 */
static List *
parse_name_or_signature(char *qualname, bool *is_signature)
{
	char	   *nextp;
	char	   *rawname;
	bool		after_dot = false;
	List	   *result = NIL;

	/* We need a modifiable copy of the input string. */
	rawname = pstrdup(qualname);

	/*
	 * The code below scribbles on qualname_str in some cases, so we should
	 * reconvert qualname if we need to show the original string in error
	 * messages.
	 */
	nextp = rawname;

	/* skip leading whitespace */
	while (scanner_isspace(*nextp))
		nextp++;

	for (;;)
	{
		char	   *curname;
		bool		missing_ident = true;

		if (*nextp == '"')
		{
			char	   *endp;

			curname = nextp + 1;
			for (;;)
			{
				endp = strchr(nextp + 1, '"');
				if (endp == NULL)
					ereport(ERROR,
							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
							 errmsg("string is not a valid identifier: \"%s\"",
									qualname),
							 errdetail("String has unclosed double quotes.")));
				if (endp[1] != '"')
					break;
				memmove(endp, endp + 1, strlen(endp));
				nextp = endp;
			}
			nextp = endp + 1;
			*endp = '\0';

			if (endp - curname == 0)
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
						 errmsg("string is not a valid identifier: \"%s\"",
								qualname),
						 errdetail("Quoted identifier must not be empty.")));

			truncate_identifier(curname, strlen(curname), true);
			result = lappend(result, makeString(curname));

			missing_ident = false;
		}
		else if (is_ident_start((unsigned char) *nextp))
		{
			char	   *downname;
			size_t		len;

			curname = nextp++;
			while (is_ident_cont((unsigned char) *nextp))
				nextp++;

			len = nextp - curname;

			/*
			 * We don't implicitly truncate identifiers. This is useful for
			 * allowing the user to check for specific parts of the identifier
			 * being too long. It's easy enough for the user to get the
			 * truncated names by casting our output to name[].
			 */
			downname = downcase_truncate_identifier(curname, (int) len, false);
			result = lappend(result, makeString(downname));
			missing_ident = false;
		}

		if (missing_ident)
		{
			/* Different error messages based on where we failed. */
			if (*nextp == '.')
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
						 errmsg("string is not a valid identifier: \"%s\"",
								qualname),
						 errdetail("No valid identifier before \".\".")));
			else if (after_dot)
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
						 errmsg("string is not a valid identifier: \"%s\"",
								qualname),
						 errdetail("No valid identifier after \".\".")));
			else
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
						 errmsg("string is not a valid identifier: \"%s\"",
								qualname)));
		}

		while (scanner_isspace(*nextp))
			nextp++;

		if (*nextp == '.')
		{
			after_dot = true;
			nextp++;
			while (scanner_isspace(*nextp))
				nextp++;
		}
		else if (*nextp == '\0')
		{
			break;
		}
		else if (*nextp == '(')
		{
			*is_signature = true;
			return NIL;
		}
		else
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("string is not a valid identifier: \"%s\"",
							qualname)));
	}

	*is_signature = false;

	return result;
}

/*
 * Returns Oid of function specified by name or by signature
 *
 */
Oid
plpgsql_check_parse_name_or_signature(char *name_or_signature)
{
	List	   *names;
	bool		is_signature;

	names = parse_name_or_signature(name_or_signature, &is_signature);

	if (!is_signature)
	{
		FuncCandidateList clist;

		clist = FuncnameGetCandidates(names, -1, NIL, false, false,
#if PG_VERSION_NUM >= 140000
									  false,
#endif
									  true);

		if (clist == NULL)
			ereport(ERROR,
					(errcode(ERRCODE_UNDEFINED_FUNCTION),
					 errmsg("function \"%s\" does not exist", name_or_signature)));
		else if (clist->next != NULL)
			ereport(ERROR,
					(errcode(ERRCODE_AMBIGUOUS_FUNCTION),
					 errmsg("more than one function named \"%s\"",
							name_or_signature)));

		return clist->oid;
	}

	return DatumGetObjectId(DirectFunctionCall1(regprocedurein,
												CStringGetDatum(name_or_signature)));
}

/*
 * Tokenize text. Only one error is possible here - unclosed double quotes.
 * Returns NULL, when found EOL or on error.
 */
static PragmaTokenType *
get_token(TokenizerState *state, PragmaTokenType *token)
{
	if (state->saved_token_is_valid)
	{
		state->saved_token_is_valid = false;
		return &state->saved_token;
	}

	/* skip inital spaces */
	while (*state->str == ' ')
		state->str++;

	if (!*state->str)
		return NULL;

	if (isdigit(*state->str))
	{
		bool	have_dot = false;

		token->value = PRAGMA_TOKEN_NUMBER;
		token->substr = state->str++;

		while (isdigit(*state->str) || (*state->str == '.'))
		{
			if (*state->str == '.')
			{
				if (!have_dot)
					have_dot = true;
				else
					break;
			}

			state->str += 1;
		}
	}
	else if (*state->str == '"')
	{
		bool	is_error = true;

		token->value = PRAGMA_TOKEN_QIDENTIF;
		token->substr = state->str++;

		is_error = true;

		while (*state->str)
		{
			if (*state->str == '"')
			{
				state->str += 1;
				if (*state->str != '"')
				{
					is_error = false;
					break;
				}
			}

			state->str += 1;
		}

		if (is_error)
			elog(ERROR, "Syntax error (unclosed quoted identifier)");
	}
	else if (*state->str == '\'')
	{
		bool	is_error = true;

		token->value = PRAGMA_TOKEN_STRING;
		token->substr = state->str++;

		is_error = true;

		while (*state->str)
		{
			if (*state->str == '\'')
			{
				state->str += 1;
				if (*state->str != '\'')
				{
					is_error = false;
					break;
				}
			}

			state->str += 1;
		}

		if (is_error)
			elog(ERROR, "Syntax error (unclosed quoted identifier)");
	}
	else if (is_ident_start(*state->str))
	{
		token->value = PRAGMA_TOKEN_IDENTIF;
		token->substr = state->str++;

		while (is_ident_cont(*state->str))
			state->str += 1;
	}
	else
		token->value = *state->str++;

	token->size = state->str - token->substr;

	return token;
}

static void
unget_token(TokenizerState *state, PragmaTokenType *token)
{
	if (token)
	{
		state->saved_token.value = token->value;
		state->saved_token.substr = token->substr;
		state->saved_token.size = token->size;

		state->saved_token_is_valid = true;
	}
	else
		state->saved_token_is_valid = false;
}

static bool
token_is_keyword(PragmaTokenType *token, const char *str)
{
	if (!token)
		return false;

	if (token->value == PRAGMA_TOKEN_IDENTIF &&
			token->size == strlen(str) &&
			strncasecmp(token->substr, str, token->size) == 0)
		return true;

	return false;
}

/*
 * Returns true if all tokens was read
 */
static bool
tokenizer_eol(TokenizerState *state)
{
	if (state->saved_token_is_valid)
		return false;

	while (*state->str)
	{
		if (!isspace(*state->str))
			return false;

		state->str += 1;
	}

	return true;
}

static void
initialize_tokenizer(TokenizerState *state, const char *str)
{
	state->str = str;
	state->saved_token_is_valid = false;
}

static char *
make_ident(PragmaTokenType *token)
{
	if (token->value == PRAGMA_TOKEN_IDENTIF)
	{
		return downcase_truncate_identifier(token->substr,
											(int) token->size,
											false);
	}
	else if (token->value == PRAGMA_TOKEN_QIDENTIF)
	{
		char	   *result = palloc(token->size);
		const char *ptr = token->substr + 1;
		char	   *write_ptr;
		size_t		n = token->size - 2;

		write_ptr = result;

		while (n-- > 0)
		{
			*write_ptr++ = *ptr;
			if (*ptr++ == '"')
			{
				ptr += 1;
				n -= 1;
			}
		}

		*write_ptr = '\0';

		truncate_identifier(result, (int) (write_ptr - result), false);

		return result;
	}
	else if (token->value == PRAGMA_TOKEN_STRING)
	{
		char	   *str = make_string(token);

		/* does same conversion like varchar->name */
		truncate_identifier(str, (int) strlen(str), false);

		return str;
	}

	return NULL;
}

static char *
make_string(PragmaTokenType *token)
{
	if (token->value == PRAGMA_TOKEN_IDENTIF ||
		token->value == PRAGMA_TOKEN_QIDENTIF)
		return make_ident(token);
	else if (token->value == PRAGMA_TOKEN_NUMBER)
		return pnstrdup(token->substr, token->size);
	else if (token->value == PRAGMA_TOKEN_STRING)
	{
		char	   *result = palloc(token->size);
		const char *ptr = token->substr + 1;
		char	   *write_ptr;
		size_t		n = token->size - 2;

		write_ptr = result;

		while (n-- > 0)
		{
			*write_ptr++ = *ptr;
			if (*ptr++ == '\'')
			{
				ptr += 1;
				n -= 1;
			}
		}

		*write_ptr = '\0';

		return result;
	}

	return NULL;
}

/*
 * Returns list of strings used in qualified identifiers
 */
static List *
get_qualified_identifier(TokenizerState *state, List *result)
{
	PragmaTokenType	token, *_token;
	bool	read_atleast_one = false;

	while (1)
	{
		_token = get_token(state, &token);
		if (!_token)
			break;

		if (_token->value != PRAGMA_TOKEN_IDENTIF && _token->value != PRAGMA_TOKEN_QIDENTIF)
			elog(ERROR, "Syntax error (expected identifier)");

		result = lappend(result, make_ident(_token));
		read_atleast_one = true;

		_token = get_token(state, &token);
		if (!_token)
			break;

		if (_token->value != '.')
		{
			unget_token(state, _token);
			break;
		}
	}

	if (!read_atleast_one)
		elog(ERROR, "Syntax error (expected identifier)");

	return result;
}

/*
 * Set start position and length of qualified identifier. Returns true,
 * if parsed identifier is valid.
 */
static void
parse_qualified_identifier(TokenizerState *state, const char **startptr, size_t *size)
{
	PragmaTokenType	token, *_token;
	bool		read_atleast_one = false;
	const char	   *_startptr = *startptr;
	size_t			_size = *size;

	while (1)
	{
		_token = get_token(state, &token);
		if (!_token)
			break;

		if (_token->value != PRAGMA_TOKEN_IDENTIF && _token->value != PRAGMA_TOKEN_QIDENTIF)
			elog(ERROR, "Syntax error (expected identifier)");

		if (!_startptr)
		{
			_startptr = _token->substr;
			_size = _token->size;
		}
		else
			_size = _token->substr - _startptr + _token->size;

		read_atleast_one = true;

		_token = get_token(state, &token);
		if (!_token)
			break;

		if (_token->value != '.')
		{
			unget_token(state, _token);
			break;
		}
	}

	if (!read_atleast_one)
		elog(ERROR, "Syntax error (expected identifier)");

	*startptr = _startptr;
	*size = _size;
}

/*
 * When rectype is not allowed, then composite type is allowed only
 * on top level.
 */
static Oid
get_type_internal(TokenizerState *state, int32 *typmod, bool allow_rectype, bool istop)
{
	PragmaTokenType	token, *_token;
	const char	   *typename_start = NULL;
	size_t			typename_length = 0;
	const char *typestr;
	TypeName   *typeName = NULL;
	Oid			typtype;

	_token = get_token(state, &token);
	if (!_token)
		elog(ERROR, "Syntax error (expected identifier)");

	if (_token->value == '(')
	{
		TupleDesc	resultTupleDesc;
		List	   *names = NIL;
		List	   *types = NIL;
		List	   *typmods = NIL;
		List	   *collations = NIL;

		if (!allow_rectype && !istop)
			elog(ERROR, "Cannot to create table with pseudo-type record.");

		_token = get_token(state, &token);
		if (token_is_keyword(_token, "like"))
		{
			typtype = get_type_internal(state, typmod, allow_rectype, false);
			if (!type_is_rowtype(typtype))
				elog(ERROR, "\"%s\" is not composite type",
								  format_type_be(typtype));

			_token = get_token(state, &token);
			if (!_token ||
					_token->value != ')')
				elog(ERROR, "Syntax error (expected \")\")");

			return typtype;
		}
		else
			unget_token(state, _token);

		while (1)
		{
			Oid		_typtype;
			int32	_typmod;

			_token = get_token(state, &token);
			if (!_token ||
				(_token->value != PRAGMA_TOKEN_IDENTIF &&
				 _token->value != PRAGMA_TOKEN_QIDENTIF))
				elog(ERROR, "Syntax error (expected identifier)");

			names = lappend(names, makeString(make_ident(_token)));

			_typtype = get_type_internal(state, &_typmod, allow_rectype, false);

			types = lappend_oid(types, _typtype);
			typmods = lappend_int(typmods, _typmod);
			collations = lappend_oid(collations, InvalidOid);

			_token = get_token(state, &token);
			if (!_token)
				elog(ERROR, "Syntax error (unclosed composite type definition - expected \")\")");

			if (_token->value == ')')
			{
				break;
			}
			else if (_token->value != ',')
				elog(ERROR, "Syntax error (expected \",\")");
		}

		resultTupleDesc = BuildDescFromLists(names, types, typmods, collations);
		resultTupleDesc = BlessTupleDesc(resultTupleDesc);

		*typmod = resultTupleDesc->tdtypmod;

		return resultTupleDesc->tdtypeid;
	}
	else if (_token->value == PRAGMA_TOKEN_QIDENTIF)
	{
		unget_token(state, _token);

		parse_qualified_identifier(state, &typename_start, &typename_length);
	}
	else if (_token->value == PRAGMA_TOKEN_IDENTIF)
	{
		PragmaTokenType	token2, *_token2;

		_token2 = get_token(state, &token2);

		if (_token2)
		{
			if (_token2->value == '.')
			{
				typename_start = _token->substr;
				typename_length = _token->size;

				parse_qualified_identifier(state, &typename_start, &typename_length);
			}
			else
			{
				/* multi word type name */
				typename_start = _token->substr;
				typename_length = _token->size;

				while (_token2 && _token2->value == PRAGMA_TOKEN_IDENTIF)
				{
					typename_length = _token2->substr + _token2->size - typename_start;

					_token2 = get_token(state, &token2);
				}

				unget_token(state, _token2);
			}
		}
		else
		{
			typename_start = _token->substr;
			typename_length = _token->size;
		}
	}
	else
		elog(ERROR, "Syntax error (expected identifier)");

	/* get typmod */
	_token = get_token(state, &token);
	if (_token)
	{
		if (_token->value == '(')
		{
			while (1)
			{
				_token = get_token(state, &token);
				if (!_token || _token->value != PRAGMA_TOKEN_NUMBER)
					elog(ERROR, "Syntax error (expected number for typmod specification)");

				_token = get_token(state, &token);
				if (!_token)
					elog(ERROR, "Syntax error (unclosed typmod specification)");

				if (_token->value == ')')
				{
					break;
				}
				else if (_token->value != ',')
					elog(ERROR, "Syntax error (expected \",\" in typmod list)");
			}

			typename_length = _token->substr + _token->size - typename_start;
		}
		else
			unget_token(state, _token);
	}

	/* get array symbols */
	_token = get_token(state, &token);
	if (_token)
	{
		if (_token->value == '[')
		{
			_token = get_token(state, &token);
			if (_token && _token->value == PRAGMA_TOKEN_NUMBER)
				_token = get_token(state, &token);

			if (!_token)
				elog(ERROR, "Syntax error (unclosed array specification)");

			if (_token->value != ']')
					elog(ERROR, "Syntax error (expected \"]\")");

			typename_length = _token->substr + _token->size - typename_start;
		}
		else
			unget_token(state, _token);
	}

	typestr = pnstrdup(typename_start, typename_length);


#if PG_VERSION_NUM >= 160000

	typeName = typeStringToTypeName(typestr, NULL);

#else

	typeName = typeStringToTypeName(typestr);

#endif

	typenameTypeIdAndMod(NULL, typeName, &typtype, typmod);

	return typtype;
}

static Oid
get_type(TokenizerState *state, int32 *typmod, bool allow_rectype)
{
	return get_type_internal(state, typmod, allow_rectype, true);
}

static int
get_varno(PLpgSQL_nsitem *cur_ns, List *names)
{
	char	   *name1 = NULL;
	char	   *name2 = NULL;
	char	   *name3 = NULL;
	int			names_used;
	PLpgSQL_nsitem *nsitem;

	switch (list_length(names))
	{
		case 1:
		{
			name1 = (char *) linitial(names);
			break;
		}
		case 2:
		{
			name1 = (char *) linitial(names);
			name2 = (char *) lsecond(names);
			break;
		}
		case 3:
		{
			name1 = (char *) linitial(names);
			name2 = (char *) lsecond(names);
			name3 = (char *) lthird(names);
			break;
		}
		default:
			return -1;
	}

	nsitem = plpgsql_check__ns_lookup_p(cur_ns, false, name1, name2, name3, &names_used);

	return nsitem ? nsitem->itemno : -1;
}

static char *
get_name(List *names)
{
	bool	is_first = true;
	ListCell *lc;

	StringInfoData sinfo;

	initStringInfo(&sinfo);

	foreach(lc, names)
	{
		if (is_first)
			is_first = false;
		else
			appendStringInfoChar(&sinfo, '.');

		appendStringInfo(&sinfo, "\"%s\"", (char *) lfirst(lc));
	}

	return sinfo.data;
}

bool
plpgsql_check_pragma_type(PLpgSQL_checkstate *cstate,
							 const char *str,
							 PLpgSQL_nsitem *ns,
							 int lineno)
{
	MemoryContext oldCxt;
	ResourceOwner oldowner;
	volatile bool result = true;

	/*
	 * namespace is available only in compile check mode, and only in this mode
	 * this pragma can be used.
	 */
	if (!ns || !cstate)
		return true;

	oldCxt = CurrentMemoryContext;

	oldowner = CurrentResourceOwner;
	BeginInternalSubTransaction(NULL);
	MemoryContextSwitchTo(cstate->check_cxt);

	PG_TRY();
	{
		TokenizerState tstate;
		int		target_dno;
		PLpgSQL_datum *target;
		List	   *names;
		Oid			typtype;
		int32		typmod;
		TupleDesc	typtupdesc;

		initialize_tokenizer(&tstate, str);

		names = get_qualified_identifier(&tstate, NULL);
		if ((target_dno = get_varno(ns, names)) == -1)
			elog(ERROR, "Cannot to find variable \"%s\" used in settype pragma", get_name(names));

		target = cstate->estate->datums[target_dno];
		if (target->dtype != PLPGSQL_DTYPE_REC)
			elog(ERROR, "Pragma \"settype\" can be applied only on variable of record type");

		typtype = get_type(&tstate, &typmod, true);

		if (!tokenizer_eol(&tstate))
			elog(ERROR, "Syntax error (unexpected chars after type specification)");

		typtupdesc = lookup_rowtype_tupdesc_copy(typtype, typmod);
		plpgsql_check_assign_tupdesc_dno(cstate, target_dno, typtupdesc, false);

		cstate->typed_variables = bms_add_member(cstate->typed_variables, target_dno);

		RollbackAndReleaseCurrentSubTransaction();
		MemoryContextSwitchTo(oldCxt);
		CurrentResourceOwner = oldowner;

		SPI_restore_connection();
	}
	PG_CATCH();
	{
		ErrorData  *edata;

		MemoryContextSwitchTo(cstate->check_cxt);
		edata = CopyErrorData();
		FlushErrorState();

		MemoryContextSwitchTo(oldCxt);
		FlushErrorState();

		RollbackAndReleaseCurrentSubTransaction();
		MemoryContextSwitchTo(oldCxt);
		CurrentResourceOwner = oldowner;

		/* reconnect spi */
		SPI_restore_connection();

		/* raise warning (errors in pragma can be ignored instead */
		ereport(WARNING,
				(errmsg("Pragma \"type\" on line %d is not processed.", lineno),
				 errdetail("%s", edata->message)));

		result = false;
	}
	PG_END_TRY();

	return result;
}

/*
 * Unfortunately the ephemeral tables introduced in PostgreSQL 10 cannot be
 * used for this purpose, because any DML operations are prohibited, and others
 * DML catalogue operations doesn't calculate with Ephemeral space.
 */
bool
plpgsql_check_pragma_table(PLpgSQL_checkstate *cstate, const char *str, int lineno)
{
	MemoryContext oldCxt;
	ResourceOwner oldowner;
	volatile bool result = true;

	if (!cstate)
		return true;

	oldCxt = CurrentMemoryContext;

	oldowner = CurrentResourceOwner;
	BeginInternalSubTransaction(NULL);
	MemoryContextSwitchTo(cstate->check_cxt);

	PG_TRY();
	{
		TokenizerState tstate;
		PragmaTokenType token, *_token;
		PragmaTokenType token2, *_token2;
		StringInfoData query;
		int32		typmod;

		initialize_tokenizer(&tstate, str);

		_token = get_token(&tstate, &token);
		if (!_token || (_token->value != PRAGMA_TOKEN_IDENTIF
				&& _token->value != PRAGMA_TOKEN_QIDENTIF))
			elog(ERROR, "Syntax error (expected identifier)");

		_token2 = get_token(&tstate, &token2);

		if (_token2 && _token2->value == '.')
		{
			char *nsname = make_ident(_token);

			if (strcmp(nsname, "pg_temp") != 0)
				elog(ERROR, "schema \"%s\" cannot be used in pragma \"table\" (only \"pg_temp\" schema is allowed)", nsname);

			_token = get_token(&tstate, &token);
			if (!_token || (_token->value != PRAGMA_TOKEN_IDENTIF
					&& _token->value != PRAGMA_TOKEN_QIDENTIF))
				elog(ERROR, "Syntax error (expected identifier)");

			_token2 = get_token(&tstate, &token2);
		}

		if (!_token2 || _token2->value != '(')
			elog(ERROR, "Syntax error (expected table specification)");

		unget_token(&tstate, _token2);

		(void) get_type(&tstate, &typmod, false);

		if (!tokenizer_eol(&tstate))
			elog(ERROR, "Syntax error (unexpected chars after table specification)");

		/* In this case we use parser just for syntax check and security check */
		initStringInfo(&query);
		appendStringInfoString(&query, "CREATE TEMP TABLE ");
		appendStringInfoString(&query, str);

		if (SPI_execute(query.data, false, 0) != SPI_OK_UTILITY)
			elog(NOTICE, "Cannot to create temporary table");

		ReleaseCurrentSubTransaction();
		MemoryContextSwitchTo(oldCxt);
		CurrentResourceOwner = oldowner;

		SPI_restore_connection();
	}
	PG_CATCH();
	{
		ErrorData  *edata;

		MemoryContextSwitchTo(cstate->check_cxt);
		edata = CopyErrorData();
		FlushErrorState();

		MemoryContextSwitchTo(oldCxt);
		FlushErrorState();

		RollbackAndReleaseCurrentSubTransaction();
		MemoryContextSwitchTo(oldCxt);
		CurrentResourceOwner = oldowner;

		/* reconnect spi */
		SPI_restore_connection();

		/* raise warning (errors in pragma can be ignored instead */
		ereport(WARNING,
				(errmsg("Pragma \"table\" on line %d is not processed.", lineno),
				 errdetail("%s", edata->message)));

		result = false;
	}
	PG_END_TRY();

	return result;
}

static bool
get_boolean_comment_option(TokenizerState *tstate, const char *name, plpgsql_check_info *cinfo)
{
	PragmaTokenType token, *_token;

	_token = get_token(tstate, &token);
	if (!_token)
		return true;

	if (_token->value == ',')
	{
		unget_token(tstate, _token);
		return true;
	}

	if (_token->value == '=')
	{
		_token = get_token(tstate, &token);
		if (!_token)
			elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected boolean value after \"=\")",
				 name, cinfo->fn_oid);
	}

	if (token_is_keyword(_token, "true") ||
		token_is_keyword(_token, "yes") ||
		token_is_keyword(_token, "t") ||
		token_is_keyword(_token, "on"))
		return true;
	else if (token_is_keyword(_token, "false") ||
			 token_is_keyword(_token, "no") ||
			 token_is_keyword(_token, "f") ||
			 token_is_keyword(_token, "off"))
		return false;
	else
		elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected boolean value)",
			 name, cinfo->fn_oid);

	/* fix warning C4715 on msvc */
	Assert(0);
	return false;
}

static char *
get_name_comment_option(TokenizerState *tstate, const char *name, plpgsql_check_info *cinfo)
{
	PragmaTokenType token, *_token;

	_token = get_token(tstate, &token);
	if (!_token)
		elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected option's argument of name type)",
			 name, cinfo->fn_oid);

	if (_token->value == '=')
	{
		_token = get_token(tstate, &token);
		if (!_token)
			elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected name value after \"=\")",
				 name, cinfo->fn_oid);
	}

	if (_token->value == PRAGMA_TOKEN_IDENTIF ||
		_token->value == PRAGMA_TOKEN_QIDENTIF ||
		_token->value == PRAGMA_TOKEN_STRING)
	{
		return pstrdup(make_ident(_token));
	}
	else
		elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected SQL identifier as argument)",
			 name, cinfo->fn_oid);

	/* fix warning C4715 on msvc */
	Assert(0);
	return NULL;
}

static Oid
get_type_comment_option(TokenizerState *tstate, const char *name, plpgsql_check_info *cinfo)
{
	PragmaTokenType token, *_token;

	_token = get_token(tstate, &token);
	if (!_token)
		elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected option's argument of type name)",
			 name, cinfo->fn_oid);

	if (_token->value == '=')
	{
		_token = get_token(tstate, &token);
		if (!_token)
			elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected type name after \"=\")",
				 name, cinfo->fn_oid);
	}

	if (_token->value == PRAGMA_TOKEN_IDENTIF ||
		_token->value == PRAGMA_TOKEN_QIDENTIF)
	{
		const char *typname_start = NULL;
		size_t		typname_length;
		char	   *typestr;
		Oid			typid;
		int32		typmod;

		unget_token(tstate, _token);

		parse_qualified_identifier(tstate, &typname_start, &typname_length);

		typestr = pnstrdup(typname_start, typname_length);
		parseTypeString(typestr, &typid, &typmod, false);

		return typid;
	}
	else
		elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected type identifier)",
			 name, cinfo->fn_oid);

	/* fix warning C4715 on msvc */
	Assert(0);
	return InvalidOid;
}

static Oid
get_table_comment_option(TokenizerState *tstate, const char *name, plpgsql_check_info *cinfo)
{
	PragmaTokenType token, *_token;

	_token = get_token(tstate, &token);
	if (!_token)
		elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected option's argument of table name)",
			 name, cinfo->fn_oid);

	if (_token->value == '=')
	{
		_token = get_token(tstate, &token);
		if (!_token)
			elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected table name after \"=\")",
				 name, cinfo->fn_oid);
	}

	if (_token->value == PRAGMA_TOKEN_IDENTIF ||
		_token->value == PRAGMA_TOKEN_QIDENTIF)
	{
		List	   *names;
		const char *tablename_start = NULL;
		size_t		tablename_length;
		char	   *tablenamestr;
		Oid			result;

		unget_token(tstate, _token);

		parse_qualified_identifier(tstate, &tablename_start, &tablename_length);

		tablenamestr = pnstrdup(tablename_start, tablename_length);

#if PG_VERSION_NUM >= 160000

		names = stringToQualifiedNameList(tablenamestr, NULL);

#else

		names = stringToQualifiedNameList(tablenamestr);

#endif

		/* We might not even have permissions on this relation; don't lock it. */
		result = RangeVarGetRelid(makeRangeVarFromNameList(names), NoLock, false);

		return result;
	}
	else
		elog(ERROR, "syntax error in comment option \"%s\" (fnoid: %u) (expected table identifier)",
			 name, cinfo->fn_oid);

	/* fix warning C4715 on msvc */
	Assert(0);
	return InvalidOid;
}

static bool
is_keyword(char *str, size_t bytes, const char *keyword)
{
	if (bytes != strlen(keyword))
		return false;

	if (strncasecmp(str, keyword, bytes) != 0)
		return false;

	return true;
}

char *
plpgsql_check_process_echo_string(char *str, plpgsql_check_info *cinfo)
{
	StringInfoData sinfo;

	initStringInfo(&sinfo);

	while (*str)
	{
		if (*str == '@' && str[1] == '@')
		{
			char	   *start;
			size_t		bytes;

			str += 2;
			start = str;

			while (*str && isalpha(*str))
			{
				str += 1;
			}

			bytes = str - start;
			if (is_keyword(start, bytes, "id"))
			{
				appendStringInfo(&sinfo, "%u", cinfo->fn_oid);
			}
			else if (is_keyword(start, bytes, "name"))
			{
				appendStringInfoString(&sinfo, get_func_name(cinfo->fn_oid));
			}
			else if (is_keyword(start, bytes, "signature"))
			{
				appendStringInfoString(&sinfo, format_procedure(cinfo->fn_oid));
			}
			else
				appendStringInfo(&sinfo, "@@%.*s", (int) bytes, start);
		}
		else
			appendStringInfoChar(&sinfo, *str++);
	}

	return sinfo.data;
}

static void
comment_options_parser(char *str, plpgsql_check_info *cinfo)
{
	TokenizerState tstate;
	PragmaTokenType token, *_token;

	initialize_tokenizer(&tstate, str);

	do
	{
		_token = get_token(&tstate, &token);

		if (!_token || (_token->value != PRAGMA_TOKEN_IDENTIF))
			elog(ERROR, "Syntax error (fnoid: %u) (expected option name)", cinfo->fn_oid);

		if (cinfo->incomment_options_usage_warning)
			elog(WARNING, "comment option \"%s\" is used (oid: %u)", make_ident(_token), cinfo->fn_oid);

		if (token_is_keyword(_token, "relid"))
		{
			cinfo->relid = get_table_comment_option(&tstate, "relid", cinfo);
		}
		else if (token_is_keyword(_token, "fatal_errors"))
		{
			cinfo->fatal_errors = get_boolean_comment_option(&tstate, "fatal_errors", cinfo);
		}
		else if (token_is_keyword(_token, "other_warnings"))
		{
			cinfo->other_warnings = get_boolean_comment_option(&tstate, "other_warnings", cinfo);
		}
		else if (token_is_keyword(_token, "extra_warnings"))
		{
			cinfo->extra_warnings = get_boolean_comment_option(&tstate, "extra_warnings", cinfo);
		}
		else if (token_is_keyword(_token, "performance_warnings"))
		{
			cinfo->performance_warnings = get_boolean_comment_option(&tstate, "performance_warnings", cinfo);
		}
		else if (token_is_keyword(_token, "security_warnings"))
		{
			cinfo->security_warnings = get_boolean_comment_option(&tstate, "security_warnings", cinfo);
		}
		else if (token_is_keyword(_token, "compatibility_warnings"))
		{
			cinfo->compatibility_warnings = get_boolean_comment_option(&tstate, "compatibility_warnings", cinfo);
		}
		else if (token_is_keyword(_token, "anyelementtype"))
		{
			cinfo->anyelementoid = get_type_comment_option(&tstate, "anyelementtype",cinfo);
		}
		else if (token_is_keyword(_token, "anyenumtype"))
		{
			cinfo->anyenumoid = get_type_comment_option(&tstate, "anyenumtype", cinfo);
		}
		else if (token_is_keyword(_token, "anyrangetype"))
		{
			cinfo->anyrangeoid = get_type_comment_option(&tstate, "anyrangetype", cinfo);
			if (!type_is_range(cinfo->anyrangeoid))
				elog(ERROR, "the type specified by \"anyrangetype\" comment option is not range (fnoid: %u)",
					 cinfo->fn_oid);
		}
		else if (token_is_keyword(_token, "anycompatibletype"))
		{
			cinfo->anycompatibleoid = get_type_comment_option(&tstate, "anycompatibletype", cinfo);
		}
		else if (token_is_keyword(_token, "anycompatiblerangetype"))
		{
			cinfo->anycompatiblerangeoid = get_type_comment_option(&tstate, "anycompatiblerangetype", cinfo);
			if (!type_is_range(cinfo->anycompatiblerangeoid))
				elog(ERROR, "the type specified by \"anycompatiblerangetype\" comment option is not range (fnoid: %u)",
					 cinfo->fn_oid);
		}
		else if (token_is_keyword(_token, "without_warnings"))
		{
			cinfo->without_warnings = get_boolean_comment_option(&tstate, "without_warnings", cinfo);
		}
		else if (token_is_keyword(_token, "all_warnings"))
		{
			cinfo->all_warnings = get_boolean_comment_option(&tstate, "all_warnings", cinfo);
		}
		else if (token_is_keyword(_token, "newtable"))
		{
			cinfo->newtable = get_name_comment_option(&tstate, "newtable", cinfo);
		}
		else if (token_is_keyword(_token, "oldtable"))
		{
			cinfo->oldtable = get_name_comment_option(&tstate, "oldtable", cinfo);
		}
		else if (token_is_keyword(_token, "echo"))
		{
			_token = get_token(&tstate, &token);
			if (!_token)
				elog(ERROR, "missing argument of option \"echo\"");

			if (_token->value == '=')
			{
				_token = get_token(&tstate, &token);
				if (!_token)
					elog(ERROR, "expected value after \"=\"");
			}

			if (_token->value == PRAGMA_TOKEN_IDENTIF)
				elog(NOTICE, "comment option \"echo\" is %s", plpgsql_check_process_echo_string(make_string(_token), cinfo));
			else if (_token->value == PRAGMA_TOKEN_QIDENTIF)
				elog(NOTICE, "comment option \"echo\" is \"%s\"", plpgsql_check_process_echo_string(make_string(_token), cinfo));
			else if (_token->value == PRAGMA_TOKEN_NUMBER)
				elog(NOTICE, "comment option \"echo\" is %s", plpgsql_check_process_echo_string(make_string(_token), cinfo));
			else if (_token->value == PRAGMA_TOKEN_STRING)
				elog(NOTICE, "comment option \"echo\" is '%s'", plpgsql_check_process_echo_string(make_string(_token), cinfo));
			else
				elog(NOTICE, "comment option \"echo\" is '%c'", _token->value);
		}
		else
			elog(ERROR, "unsupported option \"%.*s\" specified by \"@plpgsql_check_options\" (fnoid: %u)",
				 (int) _token->size, _token->substr, cinfo->fn_oid);

		_token = get_token(&tstate, &token);
		if (!_token)
			break;

		if (_token->value != ',')
			elog(ERROR, "expected \",\" or end of line on line with \"@plpgsql_check_options\" options (fnoid: %u)",
				 cinfo->fn_oid);
	}
	while (_token);
}


static void
comment_options_parsecontent(char *str, size_t bytes, plpgsql_check_info *cinfo)
{
	char *endchar = str + bytes;

	do
	{
		char	   *ptr, *optsline;
		bool		found_eol;

		str += strlen(tagstr);

		Assert(str <= endchar);

		/* find end of line */
		ptr = str; found_eol = false;

		while (ptr < endchar && *ptr)
		{
			if (*ptr == '\n')
			{
				found_eol = true;
				break;
			}

			ptr += 1;
		}

		optsline = pnstrdup(str, found_eol ? ptr - str : endchar - str);

		comment_options_parser(optsline, cinfo);

		pfree(optsline);

		if (!found_eol || ptr >= endchar)
			break;

		str = memmem(ptr + 1, endchar - (ptr + 1),
					 tagstr, strlen(tagstr));
	}
	while (str);
}

static char *
search_comment_options_linecomment(char *src, plpgsql_check_info *cinfo)
{
	char	   *start = src;

	while (*src)
	{
		if (*src == '\n')
		{
			char *tag;

			tag = memmem(start, src - start,
						 tagstr, strlen(tagstr));
			if (tag)
				comment_options_parsecontent(tag, src - tag, cinfo);

			return src + 1;
		}

		src += 1;
	}

	return src;
}

static char *
search_comment_options_multilinecomment(char *src, plpgsql_check_info *cinfo)
{
	char	   *start = src;

	while (*src)
	{
		if (*src == '*' && src[1] == '/')
		{
			char *tag;

			tag = memmem(start, src - start,
						 tagstr, strlen(tagstr));
			if (tag)
				comment_options_parsecontent(tag, src - tag, cinfo);

			return src + 1;
		}

		src += 1;
	}

	return src;
}

/*
 * Try to read plpgsql_check options.
 *
 */
void
plpgsql_check_search_comment_options(plpgsql_check_info *cinfo)
{
	char *src = plpgsql_check_get_src(cinfo->proctuple);

	cinfo->all_warnings = false;
	cinfo->without_warnings = false;

	while (*src)
	{
		if (*src == '-' && src[1] == '-')
			src = search_comment_options_linecomment(src + 2, cinfo);

		else if (*src == '/' && src[1] == '*')
			src = search_comment_options_multilinecomment(src + 2, cinfo);

		else if (*src == '\'')
		{
			src++;

			while (*src)
			{
				if (*src++ == '\'')
				{
					if (*src == '\'')
						src += 1;
					else
						break;
				}
			}
		}

		else if (*src == '"')
		{
			src++;

			while (*src)
			{
				if (*src++ == '"')
				{
					if (*src == '"')
						src += 1;
					else
						break;
				}
			}
		}

		else if (*src == '$')
		{
			char	   *start = src++;
			bool		is_custom_string = false;

			while (*src)
			{
				if (isblank(*src))
				{
					is_custom_string = false;
					break;
				}
				else if (*src == '$')
				{
					is_custom_string = true;
					break;
				}

				src += 1;
			}

			if (is_custom_string)
			{
				size_t			cust_str_length = 0;

				cust_str_length = src - start + 1;

next_char:

				src += 1;

				while (*src)
				{
					size_t		i;

					for (i = 0; i < cust_str_length; i++)
					{
						if (src[i] != start[i])
							goto next_char;
					}

					/* found complete custom string separator */
					src += cust_str_length;
					break;
				}
			}
			else
				src = start + 1;
		}
		else
			src += 1;
	}

	if (cinfo->all_warnings && cinfo->without_warnings)
		elog(ERROR, "all_warnings and without_warnings cannot be used together (fnoid: %u)",
			 cinfo->fn_oid);

	if (cinfo->all_warnings)
		plpgsql_check_set_all_warnings(cinfo);
	else if (cinfo->without_warnings)
		plpgsql_check_set_without_warnings(cinfo);
}
