/*
 * This file and its contents are licensed under the Apache License 2.0.
 * Please see the included NOTICE for copyright information and
 * LICENSE-APACHE for a copy of the license.
 */

#include <postgres.h>
#include <access/htup_details.h>
#include <catalog/dependency.h>
#include <catalog/namespace.h>
#include <catalog/pg_trigger.h>
#include <catalog/pg_type.h>
#include <commands/trigger.h>
#include <fmgr.h>
#include <parser/parser.h>
#include <port.h>
#include <storage/lmgr.h>
#include <utils/builtins.h>
#include <utils/elog.h>
#include <utils/lsyscache.h>
#include <utils/typcache.h>

#include "compat/compat.h"
#include "bmslist_utils.h"
#include "cross_module_fn.h"
#include "debug_assert.h"
#include "guc.h"
#include "jsonb_utils.h"
#include "ts_catalog/array_utils.h"
#include "ts_catalog/compression_settings.h"

#include "alter_table_with_clause.h"

static const WithClauseDefinition alter_table_with_clause_def[] = {
		[AlterTableFlagChunkTimeInterval] = {
			.arg_names = {"chunk_interval", NULL},
			 .type_id = TEXTOID,
		},
		[AlterTableFlagColumnstore] = {
			.arg_names = {"compress", "columnstore", "enable_columnstore", NULL},
			.type_id = BOOLOID,
			.default_val = (Datum)false,
		},
		[AlterTableFlagSegmentBy] = {
			.arg_names = {"compress_segmentby", "segmentby", "segment_by", NULL},
			 .type_id = TEXTOID,
		},
		[AlterTableFlagOrderBy] = {
			.arg_names = {"compress_orderby", "orderby", "order_by", NULL},
			 .type_id = TEXTOID,
		},
		[AlterTableFlagCompressChunkTimeInterval] = {
			.arg_names = {"compress_chunk_interval", "compress_chunk_time_interval", NULL},
			 .type_id = INTERVALOID,
		},
		[AlterTableFlagIndex] = {
			.arg_names = {"compress_index", "compress_sparse_index", "index", "sparse_index", NULL},
			 .type_id = TEXTOID,
		},
};

static const WithClauseDefinition sparse_index_with_clause_def[] = {
	[_SparseIndexTypeEnumBloom] = {
		.arg_names = {"compress_bloom", "bloom", NULL},
		 .type_id = TEXTOID,
	},
	[_SparseIndexTypeEnumMinmax] = {
		.arg_names = {"compress_minmax", "minmax", "compress_min_max", "min_max", NULL},
		.type_id = TEXTOID,
	},
	[_SparseIndexTypeEnumFirstLast] = {
		.arg_names = {"compress_firstlast", "firstlast", "first_last", NULL},
		.type_id = TEXTOID,
	},
};

WithClauseResult *
ts_alter_table_with_clause_parse(const List *defelems)
{
	return ts_with_clauses_parse(defelems,
								 alter_table_with_clause_def,
								 TS_ARRAY_LEN(alter_table_with_clause_def));
}

WithClauseResult *
ts_alter_table_reset_with_clause_parse(const List *defelems)
{
	return ts_with_clauses_parse_reset(defelems,
									   alter_table_with_clause_def,
									   TS_ARRAY_LEN(alter_table_with_clause_def));
}

static inline void
throw_segment_by_error(char *segment_by)
{
	ereport(ERROR,
			(errcode(ERRCODE_SYNTAX_ERROR),
			 errmsg("unable to parse segmenting option \"%s\"", segment_by),
			 errhint("The option timescaledb.compress_segmentby must"
					 " be a set of columns separated by commas.")));
}

static bool
select_stmt_as_expected(SelectStmt *stmt)
{
	/* The only parts of the select stmt that are allowed to be set are the order by or group by.
	 * Check that no other fields are set */
	if (stmt->distinctClause != NIL || stmt->intoClause != NULL || stmt->targetList != NIL ||
		stmt->whereClause != NULL || stmt->havingClause != NULL || stmt->windowClause != NIL ||
		stmt->valuesLists != NULL || stmt->limitOffset != NULL || stmt->limitCount != NULL ||
		stmt->lockingClause != NIL || stmt->withClause != NULL || stmt->op != 0 ||
		stmt->all != false || stmt->larg != NULL || stmt->rarg != NULL)
	{
		return false;
	}
	return true;
}

static ArrayType *
parse_segment_collist(char *inpstr, Hypertable *hypertable)
{
	StringInfoData buf;
	List *parsed;
	ListCell *lc;
	SelectStmt *select;
	RawStmt *raw;

	/* segmentby can have empty array */
	if (strlen(inpstr) == 0)
	{
		return ts_array_add_element_text(NULL, NULL);
	}

	initStringInfo(&buf);

	/* parse the segment by list exactly how you would a group by */
	appendStringInfo(&buf,
					 "SELECT FROM %s.%s GROUP BY %s",
					 quote_identifier(NameStr(hypertable->fd.schema_name)),
					 quote_identifier(NameStr(hypertable->fd.table_name)),
					 inpstr);

	const MemoryContext oldcontext = CurrentMemoryContext;

	PG_TRY();
	{
		parsed = raw_parser(buf.data, RAW_PARSE_DEFAULT);
	}
	PG_CATCH();
	{
		/* We do this fandango to avoid exhausting the error stack if we get
		 * anything else but a syntax error, for example, an out of memory
		 * error. */
		ErrorData *edata;
		MemoryContextSwitchTo(oldcontext);
		edata = CopyErrorData();
		FlushErrorState();
		if (edata->sqlerrcode == ERRCODE_SYNTAX_ERROR)
		{
			edata->cursorpos = edata->internalpos = 0;
			edata->detail = edata->message;
			edata->message = psprintf("unable to parse segmenting option \"%s\"", inpstr);
			edata->hint = psprintf("The option timescaledb.compress_segmentby must be a set of "
								   "columns separated by commas.");
		}
		ReThrowError(edata);
	}
	PG_END_TRY();

	if (list_length(parsed) != 1)
	{
		throw_segment_by_error(inpstr);
	}
	if (!IsA(linitial(parsed), RawStmt))
	{
		throw_segment_by_error(inpstr);
	}
	raw = linitial(parsed);

	if (!IsA(raw->stmt, SelectStmt))
	{
		throw_segment_by_error(inpstr);
	}
	select = (SelectStmt *) raw->stmt;

	if (!select_stmt_as_expected(select))
	{
		throw_segment_by_error(inpstr);
	}

	if (select->sortClause != NIL)
	{
		throw_segment_by_error(inpstr);
	}

	ArrayType *segmentby = NULL;
	foreach (lc, select->groupClause)
	{
		if (!IsA(lfirst(lc), ColumnRef))
		{
			throw_segment_by_error(inpstr);
		}

		ColumnRef *cf = lfirst(lc);
		if (list_length(cf->fields) != 1)
		{
			throw_segment_by_error(inpstr);
		}

		if (!IsA(linitial(cf->fields), String))
		{
			throw_segment_by_error(inpstr);
		}

		char *colname = strVal(linitial(cf->fields));
		AttrNumber col_attno = get_attnum(hypertable->main_table_relid, colname);
		if (col_attno == InvalidAttrNumber)
		{
			ereport(ERROR,
					(errcode(ERRCODE_SYNTAX_ERROR),
					 errmsg("column \"%s\" does not exist", colname),
					 errhint("The timescaledb.compress_segmentby option must reference a valid "
							 "column.")));
		}

		/* get normalized column name */
		colname = get_attname(hypertable->main_table_relid, col_attno, false);

		/* check if segmentby columns are distinct. */
		if (ts_array_is_member(segmentby, colname))
		{
			ereport(ERROR,
					(errcode(ERRCODE_SYNTAX_ERROR),
					 errmsg("duplicate column name \"%s\"", colname),
					 errhint("The timescaledb.compress_segmentby option must reference distinct "
							 "column.")));
		}

		segmentby = ts_array_add_element_text(segmentby, pstrdup(colname));
	}

	return segmentby;
}

static inline void
throw_order_by_error(char *order_by)
{
	ereport(ERROR,
			(errcode(ERRCODE_SYNTAX_ERROR),
			 errmsg("unable to parse ordering option \"%s\"", order_by),
			 errhint("The timescaledb.compress_orderby option must be a set of column"
					 " names with sort options, separated by commas."
					 " It is the same format as an ORDER BY clause.")));
}

/* compress_orderby is parsed same as order by in select queries */
OrderBySettings
ts_compress_parse_order_collist(char *inpstr, Hypertable *hypertable)
{
	StringInfoData buf;
	List *parsed;
	ListCell *lc;
	SelectStmt *select;
	RawStmt *raw;
	OrderBySettings settings = { 0 };

	if (strlen(inpstr) == 0)
	{
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("ordering column can not be empty"),
				 errhint("timescaledb.compress_orderby option must reference a valid "
						 "column or be removed to use default settings.")));
	}

	initStringInfo(&buf);

	/* parse the segment by list exactly how you would a order by by */
	appendStringInfo(&buf,
					 "SELECT FROM %s.%s ORDER BY %s",
					 quote_identifier(NameStr(hypertable->fd.schema_name)),
					 quote_identifier(NameStr(hypertable->fd.table_name)),
					 inpstr);

	const MemoryContext oldcontext = CurrentMemoryContext;

	PG_TRY();
	{
		parsed = raw_parser(buf.data, RAW_PARSE_DEFAULT);
	}
	PG_CATCH();
	{
		/* We do this fandango to avoid exhausting the error stack if we get
		 * anything else but a syntax error, for example, an out of memory
		 * error. */
		ErrorData *edata;
		MemoryContextSwitchTo(oldcontext);
		edata = CopyErrorData();
		FlushErrorState();
		if (edata->sqlerrcode == ERRCODE_SYNTAX_ERROR)
		{
			edata->cursorpos = edata->internalpos = 0;
			edata->detail = edata->message;
			edata->message = psprintf("unable to parse ordering option \"%s\"", inpstr);
			edata->hint = psprintf("The timescaledb.compress_orderby option must be a set of column"
								   " names with sort options, separated by commas."
								   " It is the same format as an ORDER BY clause.");
		}
		ReThrowError(edata);
	}
	PG_END_TRY();

	if (list_length(parsed) != 1)
	{
		throw_order_by_error(inpstr);
	}
	if (!IsA(linitial(parsed), RawStmt))
	{
		throw_order_by_error(inpstr);
	}
	raw = linitial(parsed);
	if (!IsA(raw->stmt, SelectStmt))
	{
		throw_order_by_error(inpstr);
	}
	select = (SelectStmt *) raw->stmt;

	if (!select_stmt_as_expected(select))
	{
		throw_order_by_error(inpstr);
	}

	if (select->groupClause != NIL)
	{
		throw_order_by_error(inpstr);
	}

	foreach (lc, select->sortClause)
	{
		SortBy *sort_by;
		ColumnRef *cf;
		CompressedParsedCol *col = (CompressedParsedCol *) palloc(sizeof(*col));
		bool desc, nullsfirst;

		if (!IsA(lfirst(lc), SortBy))
		{
			throw_order_by_error(inpstr);
		}
		sort_by = lfirst(lc);

		if (!IsA(sort_by->node, ColumnRef))
		{
			throw_order_by_error(inpstr);
		}
		cf = (ColumnRef *) sort_by->node;

		if (list_length(cf->fields) != 1)
		{
			throw_order_by_error(inpstr);
		}

		if (!IsA(linitial(cf->fields), String))
		{
			throw_order_by_error(inpstr);
		}

		namestrcpy(&col->colname, strVal(linitial(cf->fields)));
		char *colname = strVal(linitial(cf->fields));

		AttrNumber col_attno = get_attnum(hypertable->main_table_relid, colname);
		if (col_attno == InvalidAttrNumber)
		{
			ereport(ERROR,
					(errcode(ERRCODE_SYNTAX_ERROR),
					 errmsg("column \"%s\" does not exist", NameStr(col->colname)),
					 errhint("The timescaledb.compress_orderby option must reference a valid "
							 "column.")));
		}

		Oid col_type = get_atttype(hypertable->main_table_relid, col_attno);
		TypeCacheEntry *type = lookup_type_cache(col_type, TYPECACHE_LT_OPR);

		if (!OidIsValid(type->lt_opr))
		{
			ereport(ERROR,
					(errcode(ERRCODE_UNDEFINED_FUNCTION),
					 errmsg("invalid ordering column type %s", format_type_be(col_type)),
					 errdetail("Could not identify a less-than operator for the type.")));
		}

		/* get normalized column name */
		colname = get_attname(hypertable->main_table_relid, col_attno, false);

		/* check if orderby columns are distinct. */
		if (ts_array_is_member(settings.orderby, colname))
		{
			ereport(ERROR,
					(errcode(ERRCODE_SYNTAX_ERROR),
					 errmsg("duplicate column name \"%s\"", colname),
					 errhint("The timescaledb.compress_orderby option must reference distinct "
							 "column.")));
		}

		if (sort_by->sortby_dir != SORTBY_ASC && sort_by->sortby_dir != SORTBY_DESC &&
			sort_by->sortby_dir != SORTBY_DEFAULT)
		{
			throw_order_by_error(inpstr);
		}

		desc = sort_by->sortby_dir == SORTBY_DESC;

		if (sort_by->sortby_nulls == SORTBY_NULLS_DEFAULT)
		{
			/* default null ordering is LAST for ASC, FIRST for DESC */
			nullsfirst = desc;
		}
		else
		{
			nullsfirst = sort_by->sortby_nulls == SORTBY_NULLS_FIRST;
		}

		settings.orderby = ts_array_add_element_text(settings.orderby, pstrdup(colname));
		settings.orderby_desc = ts_array_add_element_bool(settings.orderby_desc, desc);
		settings.orderby_nullsfirst =
			ts_array_add_element_bool(settings.orderby_nullsfirst, nullsfirst);
	}

	Ensure(settings.orderby, "orderby setting is NULL after parsing");

	return settings;
}

static inline void
throw_sparse_index_error(char *sparse_index)
{
	ereport(ERROR,
			(errcode(ERRCODE_SYNTAX_ERROR),
			 errmsg("unable to parse sparse index option \"%s\"", sparse_index)));
}

static SparseIndexTypeEnum
sparse_index_type_with_clause_parse(const char *parse, const WithClauseDefinition *args, int nargs)
{
	Assert((int) _SparseIndexTypeEnumMax == nargs);
	int i;
	for (i = 0; i < nargs; i++)
	{
		for (int j = 0; args[i].arg_names[j] != NULL; ++j)
		{
			if (pg_strcasecmp(parse, args[i].arg_names[j]) == 0)
			{
				return (SparseIndexTypeEnum) i;
			}
		}
	}

	ereport(ERROR,
			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
			 errmsg("unrecognized sparse index type \"%s\"", parse)));

	return _SparseIndexTypeEnumMax;
}

static SparseIndexColumn
parse_sparse_index_column(Hypertable *hypertable, FuncCall *sparse_index_details, int index,
						  SparseIndexTypeEnum type)
{
	SparseIndexColumn column;
	Assert(index >= 0);
	Assert(list_length(sparse_index_details->args) > index);

	if (index >= list_length(sparse_index_details->args) || index >= MAX_BLOOM_FILTER_COLUMNS)
	{
		ereport(ERROR,
				(errcode(ERRCODE_SYNTAX_ERROR),
				 errmsg("sparse index %s has too many columns", ts_sparse_index_type_names[type])));
	}

	Node *arg = list_nth(sparse_index_details->args, index);
	if (!IsA(arg, ColumnRef))
	{
		ereport(ERROR,
				(errcode(ERRCODE_SYNTAX_ERROR),
				 errmsg("sparse index column reference must reference a valid column name")));
	}

	ColumnRef *cf = (ColumnRef *) arg;
	if (list_length(cf->fields) != 1 || !IsA(linitial(cf->fields), String))
	{
		ereport(ERROR,
				(errcode(ERRCODE_SYNTAX_ERROR),
				 errmsg("invalid sparse index column reference syntax"),
				 errdetail(
					 "Wildcard or qualified references like '*' or 'table.col' are not allowed.")));
	}

	column.name = strVal(linitial(cf->fields));
	column.attnum = get_attnum(hypertable->main_table_relid, column.name);
	if (column.attnum == InvalidAttrNumber)
	{
		ereport(ERROR,
				(errcode(ERRCODE_SYNTAX_ERROR),
				 errmsg("column \"%s\" does not exist", column.name),
				 errhint("The sparse index %s option must reference a valid "
						 "column.",
						 ts_sparse_index_type_names[type])));
	}

	/* get normalized column name */
	column.name = get_attname(hypertable->main_table_relid, column.attnum, false);
	column.type = get_atttype(hypertable->main_table_relid, column.attnum);

	return column;
}

static const char *
column_name_list_as_string(BloomFilterConfig *config)
{
	StringInfoData buf;
	initStringInfo(&buf);
	appendStringInfo(&buf, "(");
	for (int i = 0; i < config->num_columns; i++)
	{
		appendStringInfo(&buf, "'%s'", config->columns[i].name);
		if (i < config->num_columns - 1)
		{
			appendStringInfo(&buf, ",");
		}
	}
	appendStringInfo(&buf, ")");
	return buf.data;
}

/* parses the individual sparse index config entities. being called once for each sparse index
 * config entity in the list. */
static void
parse_sparse_index_config(JsonbParseState *parse_state, FuncCall *sparse_index_details,
						  Hypertable *hypertable, TsBmsList *sparse_index_columns)
{
	TypeCacheEntry *type_cache;
	MinmaxIndexColumnConfig minmax_config;
	FirstLastIndexColumnConfig firstlast_config;
	BloomFilterConfig bloom_config;
	SparseIndexConfigBase config;
	SparseIndexConfigBase *config_ptr = &config;
	SparseIndexColumn first_column;

	config.type =
		sparse_index_type_with_clause_parse(NameListToString(sparse_index_details->funcname),
											sparse_index_with_clause_def,
											TS_ARRAY_LEN(sparse_index_with_clause_def));
	config.source = _SparseIndexSourceEnumConfig;
	int num_columns = list_length(sparse_index_details->args);

	if (num_columns != 1)
	{
		if (num_columns > 1 && config.type == _SparseIndexTypeEnumBloom)
		{
			/* This will be enabled once all composite bloom index functionality is rolled out */
			if (!ts_guc_enable_composite_bloom_indexes)
			{
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("composite bloom indexes are disabled"),
						 errhint("Set timescaledb.enable_composite_bloom_indexes = true")));
			}

			if (num_columns > MAX_BLOOM_FILTER_COLUMNS)
			{
				ereport(ERROR,
						(errcode(ERRCODE_SYNTAX_ERROR),
						 errmsg("bloom index has too many columns: %d > max %d",
								num_columns,
								MAX_BLOOM_FILTER_COLUMNS)));
			}
		}
		else
		{
			ereport(ERROR,
					(errcode(ERRCODE_SYNTAX_ERROR),
					 errmsg("only bloom indexes can have multiple columns")));
		}
	}

	/* parse the first column separately because we only need one for minmax */
	first_column = parse_sparse_index_column(hypertable, sparse_index_details, 0, config.type);
	Bitmapset *attnums_bitmap = bms_make_singleton(first_column.attnum);

	/* extract custom sparse index type config */
	switch (config.type)
	{
		case _SparseIndexTypeEnumBloom:
			if (!ts_guc_enable_sparse_index_bloom)
			{
				ereport(ERROR,
						(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
						 errmsg("Creating bloom sparse index is disabled"),
						 errhint("Either set \"enable_sparse_index_bloom\" to true or remove the "
								 "bloom filter indexes from \"sparse_index\" configuration of the "
								 "hypertable.")));
			}

			bloom_config.base = config;
			config_ptr = (SparseIndexConfigBase *) &bloom_config;
			bloom_config.num_columns = num_columns;

			bloom_config.columns = palloc(num_columns * sizeof(SparseIndexColumn));
			bloom_config.columns[0] = first_column;
			for (int i = 1; i < num_columns; i++)
			{
				bloom_config.columns[i] =
					parse_sparse_index_column(hypertable, sparse_index_details, i, config.type);
				attnums_bitmap = bms_add_member(attnums_bitmap, bloom_config.columns[i].attnum);
				if (bms_num_members(attnums_bitmap) <= i)
				{
					ereport(ERROR,
							(errcode(ERRCODE_SYNTAX_ERROR),
							 errmsg("duplicate column name ('%s') in composite bloom index "
									"configuration: %s",
									bloom_config.columns[i].name,
									column_name_list_as_string(&bloom_config)),
							 errhint(
								 "The sparse index option must reference distinct column set.")));
				}
			}

			if (ts_bmslist_contains_set(*sparse_index_columns, attnums_bitmap))
			{
				ereport(ERROR,
						(errcode(ERRCODE_SYNTAX_ERROR),
						 errmsg("duplicate sparse index configuration %s",
								column_name_list_as_string(&bloom_config)),
						 errhint("The sparse index option must reference distinct column set.")));
			}
			*sparse_index_columns = ts_bmslist_add_set(*sparse_index_columns, attnums_bitmap);

			for (int i = 0; i < num_columns; i++)
			{
				/*
				 * The column type must be hashable. For some types we use our own hash functions
				 * which have better characteristics.
				 */
				FmgrInfo *finfo = NULL;
				if (ts_cm_functions->bloom1_get_hash_function(bloom_config.columns[i].type,
															  &finfo) == NULL)
				{
					ereport(ERROR,
							(errcode(ERRCODE_UNDEFINED_FUNCTION),
							 errmsg("invalid bloom filter column type %s",
									format_type_be(bloom_config.columns[i].type)),
							 errdetail("Could not identify a hashing function for the type.")));
				}
			}

			/* the convention is that the column names are sorted by attribute number */
			qsort(bloom_config.columns,
				  num_columns,
				  sizeof(SparseIndexColumn),
				  ts_qsort_attrnumber_cmp);
			break;

		case _SparseIndexTypeEnumMinmax:
			if (ts_bmslist_contains_set(*sparse_index_columns, attnums_bitmap))
			{
				ereport(ERROR,
						(errcode(ERRCODE_SYNTAX_ERROR),
						 errmsg("duplicate column name \"%s\"", first_column.name),
						 errhint("The sparse index option must reference distinct "
								 "column.")));
			}
			*sparse_index_columns = ts_bmslist_add_set(*sparse_index_columns, attnums_bitmap);
			type_cache = lookup_type_cache(first_column.type, TYPECACHE_LT_OPR);

			/*
			 * a comparison operator is required for min max operations
			 */
			if (!OidIsValid(type_cache->lt_opr))
			{
				ereport(ERROR,
						(errcode(ERRCODE_UNDEFINED_FUNCTION),
						 errmsg("invalid minmax column type %s", format_type_be(first_column.type)),
						 errdetail("Could not identify a less-than operator for the type.")));
			}

			minmax_config.base = config;
			config_ptr = (SparseIndexConfigBase *) &minmax_config;
			minmax_config.col = first_column.name;
			break;

		case _SparseIndexTypeEnumFirstLast:
			if (ts_bmslist_contains_set(*sparse_index_columns, attnums_bitmap))
			{
				ereport(ERROR,
						(errcode(ERRCODE_SYNTAX_ERROR),
						 errmsg("duplicate column name \"%s\"", first_column.name),
						 errhint("The sparse index option must reference distinct "
								 "column.")));
			}
			*sparse_index_columns = ts_bmslist_add_set(*sparse_index_columns, attnums_bitmap);

			firstlast_config.base = config;
			config_ptr = (SparseIndexConfigBase *) &firstlast_config;
			firstlast_config.col = first_column.name;
			break;

		default:
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("Invalid sparse index type")));
	}

	ts_convert_sparse_index_config_to_jsonb(parse_state, config_ptr);
}

static Jsonb *
parse_sparse_index_config_list(char *inpstr, Hypertable *hypertable)
{
	StringInfoData buf;
	List *parsed;
	ListCell *lc;
	SelectStmt *select;
	RawStmt *raw;
	JsonbParseState *parse_state = NULL;

	/* sparse index can have empty input. Return [{"source":"config"}] jsonb */
	if (strlen(inpstr) == 0)
	{
		pushJsonbValue(&parse_state, WJB_BEGIN_ARRAY, NULL);
		pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL);
		ts_jsonb_add_str(parse_state,
						 ts_sparse_index_common_keys[SparseIndexKeySource],
						 ts_sparse_index_source_names[_SparseIndexSourceEnumConfig]); /* source */
		JsonbValueToJsonb(pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL));
		return JsonbValueToJsonb(pushJsonbValue(&parse_state, WJB_END_ARRAY, NULL));
	}

	initStringInfo(&buf);

	/* parse the sparse index list exactly how you would targetlist */
	appendStringInfo(&buf, "SELECT %s", inpstr);

	const MemoryContext oldcontext = CurrentMemoryContext;

	PG_TRY();
	{
		parsed = raw_parser(buf.data, RAW_PARSE_DEFAULT);
	}
	PG_CATCH();
	{
		/* We do this fandango to avoid exhausting the error stack if we get
		 * anything else but a syntax error, for example, an out of memory
		 * error. */
		ErrorData *edata;
		MemoryContextSwitchTo(oldcontext);
		edata = CopyErrorData();
		FlushErrorState();
		if (edata->sqlerrcode == ERRCODE_SYNTAX_ERROR)
		{
			edata->cursorpos = edata->internalpos = 0;
			edata->detail = edata->message;
			edata->message = psprintf("unable to parse sparse index option \"%s\"", inpstr);
		}
		ReThrowError(edata);
	}
	PG_END_TRY();

	if (list_length(parsed) != 1)
	{
		throw_sparse_index_error(inpstr);
	}
	if (!IsA(linitial(parsed), RawStmt))
	{
		throw_sparse_index_error(inpstr);
	}
	raw = linitial(parsed);

	if (!IsA(raw->stmt, SelectStmt))
	{
		throw_sparse_index_error(inpstr);
	}
	select = (SelectStmt *) raw->stmt;

	if (select->targetList == NULL)
	{
		throw_sparse_index_error(inpstr);
	}

	/* json format will be
	 * [{"type": "bloom", "source":"config", "column": "u"},
	 * {"type": "minmax","source":"config", "column": "ts"},
	 * {"type": "bloom", "source":"config", "column": ["age", "gender"]}]
	 */
	pushJsonbValue(&parse_state, WJB_BEGIN_ARRAY, NULL);

	TsBmsList sparse_index_columns = ts_bmslist_create();
	foreach (lc, select->targetList)
	{
		ResTarget *target = lfirst_node(ResTarget, lc);

		if (!IsA(target->val, FuncCall))
		{
			throw_sparse_index_error(inpstr);
		}

		FuncCall *fc = (FuncCall *) target->val;

		parse_sparse_index_config(parse_state, fc, hypertable, &sparse_index_columns);
	}

	ts_bmslist_free(sparse_index_columns);
	return JsonbValueToJsonb(pushJsonbValue(&parse_state, WJB_END_ARRAY, NULL));
}

/* returns List of CompressedParsedCol
 * compress_segmentby = `col1,col2,col3`
 */
ArrayType *
ts_compress_hypertable_parse_segment_by(WithClauseResult segmentby, Hypertable *hypertable)
{
	if (!segmentby.is_default)
	{
		return parse_segment_collist(TextDatumGetCString(segmentby.parsed), hypertable);
	}
	else
	{
		return NULL;
	}
}

/* returns List of CompressedParsedCol
 * E.g. timescaledb.compress_orderby = 'col1 asc nulls first,col2 desc,col3'
 */
OrderBySettings
ts_compress_hypertable_parse_order_by(WithClauseResult orderby, Hypertable *hypertable)
{
	Ensure(!orderby.is_default, "with clause is not default");
	return ts_compress_parse_order_collist(TextDatumGetCString(orderby.parsed), hypertable);
}

/* returns List of CompressedParsedCol
 * E.g. timescaledb.compress_orderby = 'col1 asc nulls first,col2 desc,col3'
 */
Interval *
ts_compress_hypertable_parse_chunk_time_interval(WithClauseResult *parsed_options,
												 Hypertable *hypertable)
{
	if (parsed_options[AlterTableFlagCompressChunkTimeInterval].is_default == false)
	{
		Datum textarg = parsed_options[AlterTableFlagCompressChunkTimeInterval].parsed;
		return DatumGetIntervalP(textarg);
	}
	else
	{
		return NULL;
	}
}

/* returns List of CompressedParsedCol
 * compress_minmax = `col1,col2,col3`
 */
Jsonb *
ts_compress_hypertable_parse_index(WithClauseResult index, Hypertable *hypertable)
{
	if (!index.is_default)
	{
		return parse_sparse_index_config_list(TextDatumGetCString(index.parsed), hypertable);
	}
	else
	{
		return NULL;
	}
}
