diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ed02839..11f28b37c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,13 @@ accidentally triggering the load of a previous DB version.** * #3661 Fix SkipScan path generation with constant DISTINCT column * #3708 Fix crash in get_aggsplit +**Improvements** +* #3598 Improve evaluation of stable functions such as now() on access node + **Thanks** * @binakot and @sebvett for reporting an issue with DISTINCT queries * @hardikm10, @DavidPavlicek and @pafiti for reporting bugs on TRUNCATE -* @phemmer for reporting an issue with aggregate queries on multinode +* @phemmer for reporting the issues on multinode with aggregate queries and evaluation of now() ## 2.4.2 (2021-09-21) diff --git a/src/compat/compat.h b/src/compat/compat.h index 9e41c59c2..4a1d943f7 100644 --- a/src/compat/compat.h +++ b/src/compat/compat.h @@ -435,4 +435,12 @@ get_reindex_options(ReindexStmt *stmt) #define raw_parser_compat(cmd) raw_parser(cmd, RAW_PARSE_DEFAULT) #endif +#if PG14_LT +#define expand_function_arguments_compat(args, result_type, func_tuple) \ + expand_function_arguments(args, result_type, func_tuple) +#else +#define expand_function_arguments_compat(args, result_type, func_tuple) \ + expand_function_arguments(args, false, result_type, func_tuple) +#endif + #endif /* TIMESCALEDB_COMPAT_H */ diff --git a/tsl/src/fdw/deparse.c b/tsl/src/fdw/deparse.c index 33af9e85e..d1954e118 100644 --- a/tsl/src/fdw/deparse.c +++ b/tsl/src/fdw/deparse.c @@ -96,15 +96,13 @@ typedef struct foreign_glob_cxt */ typedef struct deparse_expr_cxt { - PlannerInfo *root; /* global planner state */ - RelOptInfo *foreignrel; /* the foreign relation we are planning for */ - RelOptInfo *scanrel; /* the underlying scan relation. Same as - * foreignrel, when that represents a join or - * a base relation. */ - StringInfo buf; /* output buffer to append to */ - List **params_list; /* exprs that will become remote Params */ - List **current_time_idx; /* locations in the sql output that need to - * have the current time appended */ + PlannerInfo *root; /* global planner state */ + RelOptInfo *foreignrel; /* the foreign relation we are planning for */ + RelOptInfo *scanrel; /* the underlying scan relation. Same as + * foreignrel, when that represents a join or + * a base relation. */ + StringInfo buf; /* output buffer to append to */ + List **params_list; /* exprs that will become remote Params */ DataNodeChunkAssignment *sca; } deparse_expr_cxt; @@ -732,8 +730,8 @@ build_tlist_to_deparse(RelOptInfo *foreignrel) * For a base relation fpinfo->attrs_used is used to construct SELECT clause, * hence the tlist is ignored for a base relation. * - * remote_conds is the list of conditions to be deparsed into the WHERE clause - * (or, in the case of upper relations, into the HAVING clause). + * remote_where is the list of conditions to be deparsed into the WHERE clause, + * and remote_having into the HAVING clause (this is useful for upper relations). * * If params_list is not NULL, it receives a list of Params and other-relation * Vars used in the clauses; these values must be transmitted to the data @@ -751,13 +749,11 @@ build_tlist_to_deparse(RelOptInfo *foreignrel) */ void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel, List *tlist, - List *remote_conds, List *pathkeys, bool is_subquery, - List **retrieved_attrs, List **params_list, DataNodeChunkAssignment *sca, - List **current_time_idx) + List *remote_where, List *remote_having, List *pathkeys, bool is_subquery, + List **retrieved_attrs, List **params_list, DataNodeChunkAssignment *sca) { deparse_expr_cxt context; TsFdwRelInfo *fpinfo = fdw_relinfo_get(rel); - List *quals; /* * We handle relations for foreign tables, joins between those and upper @@ -771,29 +767,13 @@ deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel, List context.foreignrel = rel; context.scanrel = IS_UPPER_REL(rel) ? fpinfo->outerrel : rel; context.params_list = params_list; - context.current_time_idx = current_time_idx; context.sca = sca; /* Construct SELECT clause */ deparseSelectSql(tlist, is_subquery, retrieved_attrs, &context); - /* - * For upper relations, the WHERE clause is built from the remote - * conditions of the underlying scan relation; otherwise, we can use the - * supplied list of remote conditions directly. - */ - if (IS_UPPER_REL(rel)) - { - TsFdwRelInfo *ofpinfo; - - ofpinfo = fdw_relinfo_get(fpinfo->outerrel); - quals = ofpinfo->remote_conds; - } - else - quals = remote_conds; - /* Construct FROM and WHERE clauses */ - deparseFromExpr(quals, &context); + deparseFromExpr(remote_where, &context); if (IS_UPPER_REL(rel)) { @@ -801,10 +781,10 @@ deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel, List appendGroupByClause(tlist, &context); /* Append HAVING clause */ - if (remote_conds) + if (remote_having) { appendStringInfoString(buf, " HAVING "); - appendConditions(remote_conds, &context, true); + appendConditions(remote_having, &context, true); } } @@ -2953,14 +2933,6 @@ appendFunctionName(Oid funcid, deparse_expr_cxt *context) proname = NameStr(procform->proname); - /* - * If the function is the 'now' function, we'll have to replace it before pushing - * it down to the data node. For now just make a note of the index at which we're - * inserting it into the sql statement. - */ - if (funcid == F_NOW && context->current_time_idx) - *context->current_time_idx = lappend_int(*context->current_time_idx, buf->len); - /* Always print the function name */ appendStringInfoString(buf, quote_identifier(proname)); diff --git a/tsl/src/fdw/deparse.h b/tsl/src/fdw/deparse.h index cb0b83d7a..20d6ea279 100644 --- a/tsl/src/fdw/deparse.h +++ b/tsl/src/fdw/deparse.h @@ -47,9 +47,9 @@ extern void classify_conditions(PlannerInfo *root, RelOptInfo *baserel, List *in extern List *build_tlist_to_deparse(RelOptInfo *foreignrel); extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel, List *tlist, - List *remote_conds, List *pathkeys, bool is_subquery, - List **retrieved_attrs, List **params_list, - DataNodeChunkAssignment *swa, List **current_time_idx); + List *remote_where, List *remote_having, List *pathkeys, + bool is_subquery, List **retrieved_attrs, List **params_list, + DataNodeChunkAssignment *swa); extern const char *get_jointype_name(JoinType jointype); extern void deparseStringLiteral(StringInfo buf, const char *val); diff --git a/tsl/src/fdw/scan_exec.c b/tsl/src/fdw/scan_exec.c index 98ccb3237..1d942b232 100644 --- a/tsl/src/fdw/scan_exec.c +++ b/tsl/src/fdw/scan_exec.c @@ -41,8 +41,6 @@ enum FdwScanPrivateIndex FdwScanPrivateServerId, /* OID list of chunk oids, used by EXPLAIN */ FdwScanPrivateChunkOids, - /* Places in the remote query that need to have the current timestamp inserted */ - FdwScanCurrentTimeIndexes, /* * String describing join i.e. names of relations being joined and types * of join, added when the scan is join @@ -193,43 +191,6 @@ fdw_scan_debug_override_current_timestamp(TimestampTz time) } #endif -/* - * This function takes a sql statement char string and list of indicies to occurrences of `now()` - * within that string and then returns a new string which will be the same sql statement, only with - * the now calls replaced with the current transaction timestamp. - */ -static char * -generate_updated_sql_using_current_timestamp(const char *original_sql, List *now_indicies) -{ - static const char string_to_replace[] = "now()"; - int replace_length = strlen(string_to_replace); - StringInfoData new_query; - ListCell *lc; - int curr_index = 0; - TimestampTz now; - - initStringInfo(&new_query); - now = GetSQLCurrentTimestamp(-1); -#ifdef TS_DEBUG - if (ts_current_timestamp_override_value >= 0) - now = ts_current_timestamp_override_value; -#endif - - foreach (lc, now_indicies) - { - int next_index = lfirst_int(lc); - - Assert(next_index < strlen(original_sql) && - strncmp(string_to_replace, original_sql + next_index, replace_length) == 0); - appendBinaryStringInfo(&new_query, original_sql + curr_index, next_index - curr_index); - appendStringInfo(&new_query, "('%s'::timestamptz)", timestamptz_to_str(now)); - curr_index = next_index + replace_length; - } - - appendStringInfo(&new_query, "%s", original_sql + curr_index); - return new_query.data; -} - static TSConnection * get_connection(ScanState *ss, Oid const server_id, Bitmapset *scanrelids, List *exprs) { @@ -278,14 +239,7 @@ fdw_scan_init(ScanState *ss, TsFdwScanState *fsstate, Bitmapset *scanrelids, Lis fdw_exprs); /* Get private info created by planner functions. */ - if (list_nth(fdw_private, FdwScanCurrentTimeIndexes) == NIL) - fsstate->query = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql)); - else - fsstate->query = - generate_updated_sql_using_current_timestamp(strVal(list_nth(fdw_private, - FdwScanPrivateSelectSql)), - list_nth(fdw_private, - FdwScanCurrentTimeIndexes)); + fsstate->query = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql)); fsstate->retrieved_attrs = (List *) list_nth(fdw_private, FdwScanPrivateRetrievedAttrs); fsstate->fetch_size = intVal(list_nth(fdw_private, FdwScanPrivateFetchSize)); @@ -479,15 +433,7 @@ fdw_scan_explain(ScanState *ss, List *fdw_private, ExplainState *es, TsFdwScanSt ExplainPropertyText("Chunks", chunk_names.data, es); } - if (list_nth(fdw_private, FdwScanCurrentTimeIndexes) != NIL) - sql = - generate_updated_sql_using_current_timestamp(strVal( - list_nth(fdw_private, - FdwScanPrivateSelectSql)), - list_nth(fdw_private, - FdwScanCurrentTimeIndexes)); - else - sql = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql)); + sql = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql)); ExplainPropertyText("Remote SQL", sql, es); diff --git a/tsl/src/fdw/scan_plan.c b/tsl/src/fdw/scan_plan.c index 57a90faa9..00a816154 100644 --- a/tsl/src/fdw/scan_plan.c +++ b/tsl/src/fdw/scan_plan.c @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #include #include @@ -30,6 +32,7 @@ #include "scan_plan.h" #include "debug.h" #include "fdw_utils.h" +#include "scan_exec.h" /* * get_useful_pathkeys_for_relation @@ -173,15 +176,274 @@ fdw_add_upper_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel, Pa add_paths_with_pathkeys_for_rel(root, rel, epq_path, NULL, create_upper_path); } +typedef struct +{ + ParamListInfo boundParams; + PlannerInfo *root; + List *active_fns; + Node *case_val; + bool estimate; +} eval_stable_functions_context; + +static Node *eval_stable_functions_mutator(Node *node, void *context); + +static Expr * +evaluate_stable_function(Oid funcid, Oid result_type, int32 result_typmod, Oid result_collid, + Oid input_collid, List *args, bool funcvariadic, Form_pg_proc funcform) +{ + bool has_nonconst_input = false; + bool has_null_input = false; + ListCell *arg; + FuncExpr *newexpr; + +#ifdef TS_DEBUG + /* Allow tests to specify the time to push down in place of now() */ + if (funcid == F_NOW && ts_current_timestamp_override_value != -1) + { + return (Expr *) makeConst(TIMESTAMPTZOID, + -1, + InvalidOid, + sizeof(TimestampTz), + TimestampTzGetDatum(ts_current_timestamp_override_value), + false, + FLOAT8PASSBYVAL); + } +#endif + + /* + * Can't simplify if it returns a set or a RECORD. See the comments for + * eval_const_expressions(). We should only see the whitelisted functions + * here, no sets or RECORDS among them. + */ + Assert(!funcform->proretset); + Assert(funcform->prorettype != RECORDOID); + + /* + * Check for constant inputs and especially constant-NULL inputs. + */ + foreach (arg, args) + { + if (IsA(lfirst(arg), Const)) + has_null_input |= ((Const *) lfirst(arg))->constisnull; + else + has_nonconst_input = true; + } + + /* + * The simplification of strict functions with constant NULL inputs must + * have been already performed by eval_const_expressions(). + */ + Assert(!(funcform->proisstrict && has_null_input)); + + /* + * Otherwise, can simplify only if all inputs are constants. (For a + * non-strict function, constant NULL inputs are treated the same as + * constant non-NULL inputs.) + */ + if (has_nonconst_input) + return NULL; + + /* + * This is called on the access node for the expressions that will be pushed + * down to data nodes. These expressions can contain only whitelisted stable + * functions, so we shouldn't see volatile functions here. Immutable + * functions can also occur here for expressions like + * `immutable(stable(....))`, after we evaluate the stable function. + */ + Assert(funcform->provolatile != PROVOLATILE_VOLATILE); + + /* + * OK, looks like we can simplify this operator/function. + * + * Build a new FuncExpr node containing the already-simplified arguments. + */ + newexpr = makeNode(FuncExpr); + newexpr->funcid = funcid; + newexpr->funcresulttype = result_type; + newexpr->funcretset = false; + newexpr->funcvariadic = funcvariadic; + newexpr->funcformat = COERCE_EXPLICIT_CALL; /* doesn't matter */ + newexpr->funccollid = result_collid; /* doesn't matter */ + newexpr->inputcollid = input_collid; + newexpr->args = args; + newexpr->location = -1; + + return evaluate_expr((Expr *) newexpr, result_type, result_typmod, result_collid); +} + +/* + * Execute the function to deliver a constant result. + */ +static Expr * +simplify_stable_function(Oid funcid, Oid result_type, int32 result_typmod, Oid result_collid, + Oid input_collid, List **args_p, bool funcvariadic) +{ + List *args = *args_p; + HeapTuple func_tuple; + Form_pg_proc funcform; + Expr *newexpr; + + func_tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid)); + if (!HeapTupleIsValid(func_tuple)) + elog(ERROR, "cache lookup failed for function %u", funcid); + funcform = (Form_pg_proc) GETSTRUCT(func_tuple); + + /* + * Process the function arguments. Here we must deal with named or defaulted + * arguments, and then recursively apply eval_stable_functions to the whole + * argument list. + */ + args = expand_function_arguments_compat(args, result_type, func_tuple); + args = (List *) expression_tree_mutator((Node *) args, eval_stable_functions_mutator, NULL); + /* Argument processing done, give it back to the caller */ + *args_p = args; + + /* Now attempt simplification of the function call proper. */ + newexpr = evaluate_stable_function(funcid, + result_type, + result_typmod, + result_collid, + input_collid, + args, + funcvariadic, + funcform); + + ReleaseSysCache(func_tuple); + + return newexpr; +} + +/* + * Recursive guts of eval_stable_functions. + * We don't use 'context' here but it is required by the signature of + * expression_tree_mutator. + */ +static Node * +eval_stable_functions_mutator(Node *node, void *context) +{ + if (node == NULL) + return NULL; + switch (nodeTag(node)) + { + case T_FuncExpr: + { + FuncExpr *expr = (FuncExpr *) node; + List *args = expr->args; + Expr *simple; + FuncExpr *newexpr; + + /* + * Code for op/func reduction is pretty bulky, so split it out + * as a separate function. Note: exprTypmod normally returns + * -1 for a FuncExpr, but not when the node is recognizably a + * length coercion; we want to preserve the typmod in the + * eventual Const if so. + */ + simple = simplify_stable_function(expr->funcid, + expr->funcresulttype, + exprTypmod(node), + expr->funccollid, + expr->inputcollid, + &args, + expr->funcvariadic); + if (simple) /* successfully simplified it */ + return (Node *) simple; + + /* + * The expression cannot be simplified any further, so build + * and return a replacement FuncExpr node using the + * possibly-simplified arguments. Note that we have also + * converted the argument list to positional notation. + */ + newexpr = makeNode(FuncExpr); + newexpr->funcid = expr->funcid; + newexpr->funcresulttype = expr->funcresulttype; + newexpr->funcretset = expr->funcretset; + newexpr->funcvariadic = expr->funcvariadic; + newexpr->funcformat = expr->funcformat; + newexpr->funccollid = expr->funccollid; + newexpr->inputcollid = expr->inputcollid; + newexpr->args = args; + newexpr->location = expr->location; + return (Node *) newexpr; + } + case T_OpExpr: + { + OpExpr *expr = (OpExpr *) node; + List *args = expr->args; + Expr *simple; + OpExpr *newexpr; + + /* + * Need to get OID of underlying function. Okay to scribble + * on input to this extent. + */ + set_opfuncid(expr); + + /* + * Code for op/func reduction is pretty bulky, so split it out + * as a separate function. + */ + simple = simplify_stable_function(expr->opfuncid, + expr->opresulttype, + -1, + expr->opcollid, + expr->inputcollid, + &args, + false); + if (simple) /* successfully simplified it */ + return (Node *) simple; + + /* + * The expression cannot be simplified any further, so build + * and return a replacement OpExpr node using the + * possibly-simplified arguments. + */ + newexpr = makeNode(OpExpr); + newexpr->opno = expr->opno; + newexpr->opfuncid = expr->opfuncid; + newexpr->opresulttype = expr->opresulttype; + newexpr->opretset = expr->opretset; + newexpr->opcollid = expr->opcollid; + newexpr->inputcollid = expr->inputcollid; + newexpr->args = args; + newexpr->location = expr->location; + return (Node *) newexpr; + } + default: + break; + } + /* + * For any node type not handled above, copy the node unchanged but + * const-simplify its subexpressions. This is the correct thing for node + * types whose behavior might change between planning and execution, such + * as CurrentOfExpr. It's also a safe default for new node types not + * known to this routine. + */ + return expression_tree_mutator((Node *) node, eval_stable_functions_mutator, NULL); +} + +/* + * Try to evaluate stable functions and operators on the access node. This + * function is similar to eval_const_expressions, but much simpler, because it + * only evaluates the functions and doesn't have to perform any additional + * canonicalizations. + */ +static Node * +eval_stable_functions(PlannerInfo *root, Node *node) +{ + return eval_stable_functions_mutator(node, NULL); +} + void fdw_scan_info_init(ScanInfo *scaninfo, PlannerInfo *root, RelOptInfo *rel, Path *best_path, List *scan_clauses) { TsFdwRelInfo *fpinfo = fdw_relinfo_get(rel); - List *remote_exprs = NIL; + List *remote_where = NIL; + List *remote_having = NIL; List *local_exprs = NIL; List *params_list = NIL; - List *current_time_idx = NIL; List *fdw_scan_tlist = NIL; List *fdw_recheck_quals = NIL; List *retrieved_attrs; @@ -225,11 +487,11 @@ fdw_scan_info_init(ScanInfo *scaninfo, PlannerInfo *root, RelOptInfo *rel, Path continue; if (list_member_ptr(fpinfo->remote_conds, rinfo)) - remote_exprs = lappend(remote_exprs, rinfo->clause); + remote_where = lappend(remote_where, rinfo->clause); else if (list_member_ptr(fpinfo->local_conds, rinfo)) local_exprs = lappend(local_exprs, rinfo->clause); else if (is_foreign_expr(root, rel, rinfo->clause)) - remote_exprs = lappend(remote_exprs, rinfo->clause); + remote_where = lappend(remote_where, rinfo->clause); else local_exprs = lappend(local_exprs, rinfo->clause); } @@ -238,7 +500,7 @@ fdw_scan_info_init(ScanInfo *scaninfo, PlannerInfo *root, RelOptInfo *rel, Path * For a base-relation scan, we have to support EPQ recheck, which * should recheck all the remote quals. */ - fdw_recheck_quals = remote_exprs; + fdw_recheck_quals = remote_where; } else if (IS_JOIN_REL(rel)) { @@ -263,8 +525,13 @@ fdw_scan_info_init(ScanInfo *scaninfo, PlannerInfo *root, RelOptInfo *rel, Path /* * Instead we get the conditions to apply from the fdw_private * structure. + * For upper relations, the WHERE clause is built from the remote + * conditions of the underlying scan relation. */ - remote_exprs = extract_actual_clauses(fpinfo->remote_conds, false); + TsFdwRelInfo *ofpinfo; + ofpinfo = fdw_relinfo_get(fpinfo->outerrel); + remote_where = extract_actual_clauses(ofpinfo->remote_conds, false); + remote_having = extract_actual_clauses(fpinfo->remote_conds, false); local_exprs = extract_actual_clauses(fpinfo->local_conds, false); /* @@ -282,6 +549,19 @@ fdw_scan_info_init(ScanInfo *scaninfo, PlannerInfo *root, RelOptInfo *rel, Path fdw_scan_tlist = build_tlist_to_deparse(rel); } + /* + * Try to locally evaluate the stable functions such as now() before pushing + * them to the remote node. + * We have to do this at the execution stage as oppossed to the planning stage, because stable + * functions must be recalculated with each execution of a prepared + * statement. + * Note that the query planner currently only pushes down to remote side + * the whitelisted stable functions, see `function_is_whitelisted()`. So + * this code only has to deal with such functions. + */ + remote_where = (List *) eval_stable_functions(root, (Node *) remote_where); + remote_having = (List *) eval_stable_functions(root, (Node *) remote_having); + /* * Build the query string to be sent for execution, and identify * expressions to be sent as parameters. @@ -291,16 +571,16 @@ fdw_scan_info_init(ScanInfo *scaninfo, PlannerInfo *root, RelOptInfo *rel, Path root, rel, fdw_scan_tlist, - remote_exprs, + remote_where, + remote_having, best_path->pathkeys, false, &retrieved_attrs, ¶ms_list, - fpinfo->sca, - ¤t_time_idx); + fpinfo->sca); /* Remember remote_exprs for possible use by PlanDirectModify */ - fpinfo->final_remote_exprs = remote_exprs; + fpinfo->final_remote_exprs = remote_where; /* * Build the fdw_private list that will be available to the executor. @@ -311,7 +591,6 @@ fdw_scan_info_init(ScanInfo *scaninfo, PlannerInfo *root, RelOptInfo *rel, Path makeInteger(fpinfo->fetch_size), makeInteger(fpinfo->server->serverid), (fpinfo->sca != NULL ? list_copy(fpinfo->sca->chunk_oids) : NIL)); - fdw_private = lappend(fdw_private, current_time_idx); Assert(!IS_JOIN_REL(rel)); if (IS_UPPER_REL(rel)) diff --git a/tsl/test/expected/dist_hypertable-12.out b/tsl/test/expected/dist_hypertable-12.out index 094288c94..3df6b8846 100644 --- a/tsl/test/expected/dist_hypertable-12.out +++ b/tsl/test/expected/dist_hypertable-12.out @@ -5247,6 +5247,373 @@ NOTICE: adding not-null constraint to column "time" (1 row) DROP TABLE test; +-- Test that stable functions are calculated on the access node. +-- +-- As a stable function to test, use the timestamp -> timestamptz conversion +-- that is stable because it uses the current timezone. +-- We have to be careful about `timestamp < timestamptz` comparison. From postgres +-- docs: +-- When comparing a timestamp without time zone to a timestamp with time zone, +-- the former value is assumed to be given in the time zone specified by the +-- TimeZone configuration parameter, and is rotated to UTC for comparison to +-- the latter value (which is already in UTC internally). +-- We don't want this to happen on data node, so we cast the filter value to +-- timestamp, and check that this cast happens on the access node and uses the +-- current timezone. +SELECT test.tsl_override_current_timestamptz(null); + tsl_override_current_timestamptz +---------------------------------- + +(1 row) + +CREATE TABLE test_tz (time timestamp, v int); +SELECT create_distributed_hypertable('test_tz','time', + chunk_time_interval => interval '1 hour'); +NOTICE: adding not-null constraint to column "time" + create_distributed_hypertable +------------------------------- + (31,public,test_tz,t) +(1 row) + +INSERT INTO test_tz VALUES ('2018-01-02 12:00:00', 2), ('2018-01-02 11:00:00', 1), + ('2018-01-02 13:00:00', 3), ('2018-01-02 14:00:00', 4); +SET TIME ZONE 'Etc/GMT'; +SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + timestamp +-------------------------- + Tue Jan 02 12:00:00 2018 +(1 row) + +-- Normal WHERE clause on baserel +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + time | v +--------------------------+--- + Tue Jan 02 13:00:00 2018 | 3 + Tue Jan 02 14:00:00 2018 | 4 +(2 rows) + +EXPLAIN (verbose, costs off) +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Custom Scan (AsyncAppend) + Output: test_tz."time", test_tz.v + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Output: test_tz_1."time", test_tz_1.v + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Index Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Output: _dist_hyper_31_97_chunk."time", _dist_hyper_31_97_chunk.v + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Output: test_tz_2."time", test_tz_2.v + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Append + -> Index Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Output: _dist_hyper_31_95_chunk."time", _dist_hyper_31_95_chunk.v + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + -> Index Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Output: _dist_hyper_31_98_chunk."time", _dist_hyper_31_98_chunk.v + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + +(27 rows) + +-- Also test different code paths used with aggregation pushdown. +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + count +------- + 2 +(1 row) + +EXPLAIN (verbose, costs off) +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Aggregate + Output: count(*) + -> Custom Scan (AsyncAppend) + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT NULL FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Result + Output: NULL::text + -> Index Only Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT NULL FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Result + Output: NULL::text + -> Append + -> Index Only Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + -> Index Only Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + +(27 rows) + +-- TODO: test HAVING here and in the later now() tests as well. +-- Change the timezone and check that the conversion is applied correctly. +SET TIME ZONE 'Etc/GMT+1'; +SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + timestamp +-------------------------- + Tue Jan 02 11:00:00 2018 +(1 row) + +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + time | v +--------------------------+--- + Tue Jan 02 13:00:00 2018 | 3 + Tue Jan 02 12:00:00 2018 | 2 + Tue Jan 02 14:00:00 2018 | 4 +(3 rows) + +EXPLAIN (verbose, costs off) +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Custom Scan (AsyncAppend) + Output: test_tz."time", test_tz.v + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Output: test_tz_1."time", test_tz_1.v + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 11:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Index Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Output: _dist_hyper_31_97_chunk."time", _dist_hyper_31_97_chunk.v + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Output: test_tz_2."time", test_tz_2.v + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 11:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Append + -> Index Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Output: _dist_hyper_31_95_chunk."time", _dist_hyper_31_95_chunk.v + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + -> Index Scan using _dist_hyper_31_96_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_96_chunk + Output: _dist_hyper_31_96_chunk."time", _dist_hyper_31_96_chunk.v + Index Cond: (_dist_hyper_31_96_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + -> Index Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Output: _dist_hyper_31_98_chunk."time", _dist_hyper_31_98_chunk.v + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + +(30 rows) + +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + count +------- + 3 +(1 row) + +EXPLAIN (verbose, costs off) +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Aggregate + Output: count(*) + -> Custom Scan (AsyncAppend) + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT NULL FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 11:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Result + Output: NULL::text + -> Index Only Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT NULL FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 11:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Result + Output: NULL::text + -> Append + -> Index Only Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + -> Index Only Scan using _dist_hyper_31_96_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_96_chunk + Index Cond: (_dist_hyper_31_96_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + -> Index Only Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + +(29 rows) + +-- Conversion to timestamptz cannot be evaluated at the access node, because the +-- argument is a column reference. +SELECT count(*) FROM test_tz WHERE time::timestamptz > now(); + count +------- + 0 +(1 row) + +-- According to our docs, JIT is not recommended for use on access node in +-- multi-node environment. Turn it off so that it doesn't ruin EXPLAIN for the +-- next query. +SET jit = 0; +-- Test that operators are evaluated as well. Comparison of timestamp with +-- timestamptz is a stable operator, and comparison of two timestamps is an +-- immutable operator. This also test that immutable functions using these +-- operators as arguments are evaluated. +EXPLAIN (verbose, costs off) +WITH dummy AS (SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp x) +SELECT * FROM test_tz, dummy +WHERE time > x + + (x = x)::int -- stable + * (x = '2018-01-02 11:00:00'::timestamp)::int -- immutable + * INTERVAL '1 hour'; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Custom Scan (AsyncAppend) + Output: test_tz."time", test_tz.v, (('Tue Jan 02 11:00:00 2018 -01'::timestamp with time zone)::timestamp without time zone) + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Output: test_tz_1."time", test_tz_1.v, ('Tue Jan 02 11:00:00 2018 -01'::timestamp with time zone)::timestamp without time zone + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Index Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Output: _dist_hyper_31_97_chunk."time", _dist_hyper_31_97_chunk.v + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Output: test_tz_2."time", test_tz_2.v, ('Tue Jan 02 11:00:00 2018 -01'::timestamp with time zone)::timestamp without time zone + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Append + -> Index Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Output: _dist_hyper_31_95_chunk."time", _dist_hyper_31_95_chunk.v + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + -> Index Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Output: _dist_hyper_31_98_chunk."time", _dist_hyper_31_98_chunk.v + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + +(27 rows) + +-- Reference value for the above test. +WITH dummy AS (SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp x) +SELECT x + (x = x)::int * (x = '2018-01-02 11:00:00'::timestamp)::int * INTERVAL '1 hour' +FROM dummy; + ?column? +-------------------------- + Tue Jan 02 12:00:00 2018 +(1 row) + +-- Check that the test function for partly overriding now() works. It's very +-- hacky and only has effect when we estimate some costs or evaluate sTABLE +-- functions in quals on access node, and has no effect in other cases. +-- Consider deleting it altogether. +SELECT test.tsl_override_current_timestamptz('2018-01-02 12:00:00 +00'::timestamptz); + tsl_override_current_timestamptz +---------------------------------- + +(1 row) + +SELECT count(*) FROM test_tz WHERE time > now(); + count +------- + 3 +(1 row) + +SELECT test.tsl_override_current_timestamptz(null); + tsl_override_current_timestamptz +---------------------------------- + +(1 row) + +RESET TIME ZONE; +DROP TABLE test_tz; +-- Check that now() is evaluated on the access node. Also check that it is evaluated +-- anew on every execution of a prepared statement. +CREATE TABLE test_now (time timestamp, v int); +SELECT create_distributed_hypertable('test_now','time', + chunk_time_interval => interval '1 hour'); +NOTICE: adding not-null constraint to column "time" + create_distributed_hypertable +------------------------------- + (32,public,test_now,t) +(1 row) + +PREPARE test_query as +SELECT count(*) FILTER (WHERE time = now()), count(*) FILTER (WHERE time < now()), + count(*) FILTER (WHERE time > now()) FROM test_now; +; +BEGIN; -- to fix the value of now(); +INSERT INTO test_now VALUES + (now(), 1), (now() + INTERVAL '1 hour', 1), + (now() + INTERVAL '2 hour', 2 ), (now() + INTERVAL '3 hour', 3); +SELECT count(*) FILTER (WHERE time = now()), count(*) FILTER (WHERE time < now()), + count(*) FILTER (WHERE time > now()) FROM test_now; + count | count | count +-------+-------+------- + 1 | 0 | 3 +(1 row) + +EXECUTE test_query; + count | count | count +-------+-------+------- + 1 | 0 | 3 +(1 row) + +-- Also test different code paths used with aggregation pushdown. +-- We can't run EXPLAIN here, because now() is different every time. But the +-- strict equality should be enough to detect if now() is being erroneously +-- evaluated on data node, where it will differ from time to time. +SELECT count(*) FROM test_now WHERE time = now(); + count +------- + 1 +(1 row) + +COMMIT; +-- now() will be different in a new transaction. +BEGIN; +SELECT count(*) FILTER (WHERE time = now()), count(*) FILTER (WHERE time < now()), + count(*) FILTER (WHERE time > now()) FROM test_now; + count | count | count +-------+-------+------- + 0 | 1 | 3 +(1 row) + +EXECUTE test_query; + count | count | count +-------+-------+------- + 0 | 1 | 3 +(1 row) + +SELECT count(*) FROM test_now WHERE time = now(); + count +------- + 0 +(1 row) + +COMMIT; +DROP TABLE test_now; +DEALLOCATE test_query; +-- Cleanup DROP DATABASE :DATA_NODE_1; DROP DATABASE :DATA_NODE_2; DROP DATABASE :DATA_NODE_3; diff --git a/tsl/test/expected/dist_hypertable-13.out b/tsl/test/expected/dist_hypertable-13.out index a91b0f420..ed242d0f5 100644 --- a/tsl/test/expected/dist_hypertable-13.out +++ b/tsl/test/expected/dist_hypertable-13.out @@ -5246,6 +5246,373 @@ NOTICE: adding not-null constraint to column "time" (1 row) DROP TABLE test; +-- Test that stable functions are calculated on the access node. +-- +-- As a stable function to test, use the timestamp -> timestamptz conversion +-- that is stable because it uses the current timezone. +-- We have to be careful about `timestamp < timestamptz` comparison. From postgres +-- docs: +-- When comparing a timestamp without time zone to a timestamp with time zone, +-- the former value is assumed to be given in the time zone specified by the +-- TimeZone configuration parameter, and is rotated to UTC for comparison to +-- the latter value (which is already in UTC internally). +-- We don't want this to happen on data node, so we cast the filter value to +-- timestamp, and check that this cast happens on the access node and uses the +-- current timezone. +SELECT test.tsl_override_current_timestamptz(null); + tsl_override_current_timestamptz +---------------------------------- + +(1 row) + +CREATE TABLE test_tz (time timestamp, v int); +SELECT create_distributed_hypertable('test_tz','time', + chunk_time_interval => interval '1 hour'); +NOTICE: adding not-null constraint to column "time" + create_distributed_hypertable +------------------------------- + (31,public,test_tz,t) +(1 row) + +INSERT INTO test_tz VALUES ('2018-01-02 12:00:00', 2), ('2018-01-02 11:00:00', 1), + ('2018-01-02 13:00:00', 3), ('2018-01-02 14:00:00', 4); +SET TIME ZONE 'Etc/GMT'; +SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + timestamp +-------------------------- + Tue Jan 02 12:00:00 2018 +(1 row) + +-- Normal WHERE clause on baserel +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + time | v +--------------------------+--- + Tue Jan 02 13:00:00 2018 | 3 + Tue Jan 02 14:00:00 2018 | 4 +(2 rows) + +EXPLAIN (verbose, costs off) +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Custom Scan (AsyncAppend) + Output: test_tz."time", test_tz.v + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Output: test_tz_1."time", test_tz_1.v + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Index Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Output: _dist_hyper_31_97_chunk."time", _dist_hyper_31_97_chunk.v + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Output: test_tz_2."time", test_tz_2.v + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Append + -> Index Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Output: _dist_hyper_31_95_chunk."time", _dist_hyper_31_95_chunk.v + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + -> Index Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Output: _dist_hyper_31_98_chunk."time", _dist_hyper_31_98_chunk.v + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + +(27 rows) + +-- Also test different code paths used with aggregation pushdown. +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + count +------- + 2 +(1 row) + +EXPLAIN (verbose, costs off) +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Aggregate + Output: count(*) + -> Custom Scan (AsyncAppend) + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT NULL FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Result + Output: NULL::text + -> Index Only Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT NULL FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Result + Output: NULL::text + -> Append + -> Index Only Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + -> Index Only Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + +(27 rows) + +-- TODO: test HAVING here and in the later now() tests as well. +-- Change the timezone and check that the conversion is applied correctly. +SET TIME ZONE 'Etc/GMT+1'; +SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + timestamp +-------------------------- + Tue Jan 02 11:00:00 2018 +(1 row) + +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + time | v +--------------------------+--- + Tue Jan 02 13:00:00 2018 | 3 + Tue Jan 02 12:00:00 2018 | 2 + Tue Jan 02 14:00:00 2018 | 4 +(3 rows) + +EXPLAIN (verbose, costs off) +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Custom Scan (AsyncAppend) + Output: test_tz."time", test_tz.v + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Output: test_tz_1."time", test_tz_1.v + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 11:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Index Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Output: _dist_hyper_31_97_chunk."time", _dist_hyper_31_97_chunk.v + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Output: test_tz_2."time", test_tz_2.v + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 11:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Append + -> Index Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Output: _dist_hyper_31_95_chunk."time", _dist_hyper_31_95_chunk.v + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + -> Index Scan using _dist_hyper_31_96_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_96_chunk + Output: _dist_hyper_31_96_chunk."time", _dist_hyper_31_96_chunk.v + Index Cond: (_dist_hyper_31_96_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + -> Index Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Output: _dist_hyper_31_98_chunk."time", _dist_hyper_31_98_chunk.v + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + +(30 rows) + +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + count +------- + 3 +(1 row) + +EXPLAIN (verbose, costs off) +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Aggregate + Output: count(*) + -> Custom Scan (AsyncAppend) + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT NULL FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 11:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Result + Output: NULL::text + -> Index Only Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT NULL FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 11:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Result + Output: NULL::text + -> Append + -> Index Only Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + -> Index Only Scan using _dist_hyper_31_96_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_96_chunk + Index Cond: (_dist_hyper_31_96_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + -> Index Only Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + +(29 rows) + +-- Conversion to timestamptz cannot be evaluated at the access node, because the +-- argument is a column reference. +SELECT count(*) FROM test_tz WHERE time::timestamptz > now(); + count +------- + 0 +(1 row) + +-- According to our docs, JIT is not recommended for use on access node in +-- multi-node environment. Turn it off so that it doesn't ruin EXPLAIN for the +-- next query. +SET jit = 0; +-- Test that operators are evaluated as well. Comparison of timestamp with +-- timestamptz is a stable operator, and comparison of two timestamps is an +-- immutable operator. This also test that immutable functions using these +-- operators as arguments are evaluated. +EXPLAIN (verbose, costs off) +WITH dummy AS (SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp x) +SELECT * FROM test_tz, dummy +WHERE time > x + + (x = x)::int -- stable + * (x = '2018-01-02 11:00:00'::timestamp)::int -- immutable + * INTERVAL '1 hour'; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Custom Scan (AsyncAppend) + Output: test_tz."time", test_tz.v, (('Tue Jan 02 11:00:00 2018 -01'::timestamp with time zone)::timestamp without time zone) + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Output: test_tz_1."time", test_tz_1.v, ('Tue Jan 02 11:00:00 2018 -01'::timestamp with time zone)::timestamp without time zone + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Index Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Output: _dist_hyper_31_97_chunk."time", _dist_hyper_31_97_chunk.v + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Output: test_tz_2."time", test_tz_2.v, ('Tue Jan 02 11:00:00 2018 -01'::timestamp with time zone)::timestamp without time zone + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Append + -> Index Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Output: _dist_hyper_31_95_chunk."time", _dist_hyper_31_95_chunk.v + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + -> Index Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Output: _dist_hyper_31_98_chunk."time", _dist_hyper_31_98_chunk.v + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + +(27 rows) + +-- Reference value for the above test. +WITH dummy AS (SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp x) +SELECT x + (x = x)::int * (x = '2018-01-02 11:00:00'::timestamp)::int * INTERVAL '1 hour' +FROM dummy; + ?column? +-------------------------- + Tue Jan 02 12:00:00 2018 +(1 row) + +-- Check that the test function for partly overriding now() works. It's very +-- hacky and only has effect when we estimate some costs or evaluate sTABLE +-- functions in quals on access node, and has no effect in other cases. +-- Consider deleting it altogether. +SELECT test.tsl_override_current_timestamptz('2018-01-02 12:00:00 +00'::timestamptz); + tsl_override_current_timestamptz +---------------------------------- + +(1 row) + +SELECT count(*) FROM test_tz WHERE time > now(); + count +------- + 3 +(1 row) + +SELECT test.tsl_override_current_timestamptz(null); + tsl_override_current_timestamptz +---------------------------------- + +(1 row) + +RESET TIME ZONE; +DROP TABLE test_tz; +-- Check that now() is evaluated on the access node. Also check that it is evaluated +-- anew on every execution of a prepared statement. +CREATE TABLE test_now (time timestamp, v int); +SELECT create_distributed_hypertable('test_now','time', + chunk_time_interval => interval '1 hour'); +NOTICE: adding not-null constraint to column "time" + create_distributed_hypertable +------------------------------- + (32,public,test_now,t) +(1 row) + +PREPARE test_query as +SELECT count(*) FILTER (WHERE time = now()), count(*) FILTER (WHERE time < now()), + count(*) FILTER (WHERE time > now()) FROM test_now; +; +BEGIN; -- to fix the value of now(); +INSERT INTO test_now VALUES + (now(), 1), (now() + INTERVAL '1 hour', 1), + (now() + INTERVAL '2 hour', 2 ), (now() + INTERVAL '3 hour', 3); +SELECT count(*) FILTER (WHERE time = now()), count(*) FILTER (WHERE time < now()), + count(*) FILTER (WHERE time > now()) FROM test_now; + count | count | count +-------+-------+------- + 1 | 0 | 3 +(1 row) + +EXECUTE test_query; + count | count | count +-------+-------+------- + 1 | 0 | 3 +(1 row) + +-- Also test different code paths used with aggregation pushdown. +-- We can't run EXPLAIN here, because now() is different every time. But the +-- strict equality should be enough to detect if now() is being erroneously +-- evaluated on data node, where it will differ from time to time. +SELECT count(*) FROM test_now WHERE time = now(); + count +------- + 1 +(1 row) + +COMMIT; +-- now() will be different in a new transaction. +BEGIN; +SELECT count(*) FILTER (WHERE time = now()), count(*) FILTER (WHERE time < now()), + count(*) FILTER (WHERE time > now()) FROM test_now; + count | count | count +-------+-------+------- + 0 | 1 | 3 +(1 row) + +EXECUTE test_query; + count | count | count +-------+-------+------- + 0 | 1 | 3 +(1 row) + +SELECT count(*) FROM test_now WHERE time = now(); + count +------- + 0 +(1 row) + +COMMIT; +DROP TABLE test_now; +DEALLOCATE test_query; +-- Cleanup DROP DATABASE :DATA_NODE_1; DROP DATABASE :DATA_NODE_2; DROP DATABASE :DATA_NODE_3; diff --git a/tsl/test/expected/dist_hypertable-14.out b/tsl/test/expected/dist_hypertable-14.out index 20218f8ff..04f72098f 100644 --- a/tsl/test/expected/dist_hypertable-14.out +++ b/tsl/test/expected/dist_hypertable-14.out @@ -5249,6 +5249,373 @@ NOTICE: adding not-null constraint to column "time" (1 row) DROP TABLE test; +-- Test that stable functions are calculated on the access node. +-- +-- As a stable function to test, use the timestamp -> timestamptz conversion +-- that is stable because it uses the current timezone. +-- We have to be careful about `timestamp < timestamptz` comparison. From postgres +-- docs: +-- When comparing a timestamp without time zone to a timestamp with time zone, +-- the former value is assumed to be given in the time zone specified by the +-- TimeZone configuration parameter, and is rotated to UTC for comparison to +-- the latter value (which is already in UTC internally). +-- We don't want this to happen on data node, so we cast the filter value to +-- timestamp, and check that this cast happens on the access node and uses the +-- current timezone. +SELECT test.tsl_override_current_timestamptz(null); + tsl_override_current_timestamptz +---------------------------------- + +(1 row) + +CREATE TABLE test_tz (time timestamp, v int); +SELECT create_distributed_hypertable('test_tz','time', + chunk_time_interval => interval '1 hour'); +NOTICE: adding not-null constraint to column "time" + create_distributed_hypertable +------------------------------- + (31,public,test_tz,t) +(1 row) + +INSERT INTO test_tz VALUES ('2018-01-02 12:00:00', 2), ('2018-01-02 11:00:00', 1), + ('2018-01-02 13:00:00', 3), ('2018-01-02 14:00:00', 4); +SET TIME ZONE 'Etc/GMT'; +SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + timestamp +-------------------------- + Tue Jan 02 12:00:00 2018 +(1 row) + +-- Normal WHERE clause on baserel +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + time | v +--------------------------+--- + Tue Jan 02 13:00:00 2018 | 3 + Tue Jan 02 14:00:00 2018 | 4 +(2 rows) + +EXPLAIN (verbose, costs off) +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Custom Scan (AsyncAppend) + Output: test_tz."time", test_tz.v + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Output: test_tz_1."time", test_tz_1.v + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Index Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Output: _dist_hyper_31_97_chunk."time", _dist_hyper_31_97_chunk.v + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Output: test_tz_2."time", test_tz_2.v + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Append + -> Index Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Output: _dist_hyper_31_95_chunk."time", _dist_hyper_31_95_chunk.v + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + -> Index Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Output: _dist_hyper_31_98_chunk."time", _dist_hyper_31_98_chunk.v + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + +(27 rows) + +-- Also test different code paths used with aggregation pushdown. +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + count +------- + 2 +(1 row) + +EXPLAIN (verbose, costs off) +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Aggregate + Output: count(*) + -> Custom Scan (AsyncAppend) + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT NULL FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Result + Output: NULL::text + -> Index Only Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT NULL FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Result + Output: NULL::text + -> Append + -> Index Only Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + -> Index Only Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + +(27 rows) + +-- TODO: test HAVING here and in the later now() tests as well. +-- Change the timezone and check that the conversion is applied correctly. +SET TIME ZONE 'Etc/GMT+1'; +SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + timestamp +-------------------------- + Tue Jan 02 11:00:00 2018 +(1 row) + +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + time | v +--------------------------+--- + Tue Jan 02 13:00:00 2018 | 3 + Tue Jan 02 12:00:00 2018 | 2 + Tue Jan 02 14:00:00 2018 | 4 +(3 rows) + +EXPLAIN (verbose, costs off) +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Custom Scan (AsyncAppend) + Output: test_tz."time", test_tz.v + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Output: test_tz_1."time", test_tz_1.v + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 11:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Index Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Output: _dist_hyper_31_97_chunk."time", _dist_hyper_31_97_chunk.v + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Output: test_tz_2."time", test_tz_2.v + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 11:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Append + -> Index Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Output: _dist_hyper_31_95_chunk."time", _dist_hyper_31_95_chunk.v + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + -> Index Scan using _dist_hyper_31_96_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_96_chunk + Output: _dist_hyper_31_96_chunk."time", _dist_hyper_31_96_chunk.v + Index Cond: (_dist_hyper_31_96_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + -> Index Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Output: _dist_hyper_31_98_chunk."time", _dist_hyper_31_98_chunk.v + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + +(30 rows) + +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + count +------- + 3 +(1 row) + +EXPLAIN (verbose, costs off) +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Aggregate + Output: count(*) + -> Custom Scan (AsyncAppend) + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT NULL FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 11:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Result + Output: NULL::text + -> Index Only Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT NULL FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 11:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Result + Output: NULL::text + -> Append + -> Index Only Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + -> Index Only Scan using _dist_hyper_31_96_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_96_chunk + Index Cond: (_dist_hyper_31_96_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + -> Index Only Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 11:00:00'::timestamp without time zone) + +(29 rows) + +-- Conversion to timestamptz cannot be evaluated at the access node, because the +-- argument is a column reference. +SELECT count(*) FROM test_tz WHERE time::timestamptz > now(); + count +------- + 0 +(1 row) + +-- According to our docs, JIT is not recommended for use on access node in +-- multi-node environment. Turn it off so that it doesn't ruin EXPLAIN for the +-- next query. +SET jit = 0; +-- Test that operators are evaluated as well. Comparison of timestamp with +-- timestamptz is a stable operator, and comparison of two timestamps is an +-- immutable operator. This also test that immutable functions using these +-- operators as arguments are evaluated. +EXPLAIN (verbose, costs off) +WITH dummy AS (SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp x) +SELECT * FROM test_tz, dummy +WHERE time > x + + (x = x)::int -- stable + * (x = '2018-01-02 11:00:00'::timestamp)::int -- immutable + * INTERVAL '1 hour'; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Custom Scan (AsyncAppend) + Output: test_tz."time", test_tz.v, (('Tue Jan 02 11:00:00 2018 -01'::timestamp with time zone)::timestamp without time zone) + -> Append + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_1 + Output: test_tz_1."time", test_tz_1.v, ('Tue Jan 02 11:00:00 2018 -01'::timestamp with time zone)::timestamp without time zone + Data node: db_dist_hypertable_1 + Chunks: _dist_hyper_31_97_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Index Scan using _dist_hyper_31_97_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_97_chunk + Output: _dist_hyper_31_97_chunk."time", _dist_hyper_31_97_chunk.v + Index Cond: (_dist_hyper_31_97_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + + -> Custom Scan (DataNodeScan) on public.test_tz test_tz_2 + Output: test_tz_2."time", test_tz_2.v, ('Tue Jan 02 11:00:00 2018 -01'::timestamp with time zone)::timestamp without time zone + Data node: db_dist_hypertable_2 + Chunks: _dist_hyper_31_95_chunk, _dist_hyper_31_96_chunk, _dist_hyper_31_98_chunk + Remote SQL: SELECT "time", v FROM public.test_tz WHERE _timescaledb_internal.chunks_in(public.test_tz.*, ARRAY[43, 44, 45]) AND (("time" > '2018-01-02 12:00:00'::timestamp without time zone)) + Remote EXPLAIN: + Append + -> Index Scan using _dist_hyper_31_95_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_95_chunk + Output: _dist_hyper_31_95_chunk."time", _dist_hyper_31_95_chunk.v + Index Cond: (_dist_hyper_31_95_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + -> Index Scan using _dist_hyper_31_98_chunk_test_tz_time_idx on _timescaledb_internal._dist_hyper_31_98_chunk + Output: _dist_hyper_31_98_chunk."time", _dist_hyper_31_98_chunk.v + Index Cond: (_dist_hyper_31_98_chunk."time" > '2018-01-02 12:00:00'::timestamp without time zone) + +(27 rows) + +-- Reference value for the above test. +WITH dummy AS (SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp x) +SELECT x + (x = x)::int * (x = '2018-01-02 11:00:00'::timestamp)::int * INTERVAL '1 hour' +FROM dummy; + ?column? +-------------------------- + Tue Jan 02 12:00:00 2018 +(1 row) + +-- Check that the test function for partly overriding now() works. It's very +-- hacky and only has effect when we estimate some costs or evaluate sTABLE +-- functions in quals on access node, and has no effect in other cases. +-- Consider deleting it altogether. +SELECT test.tsl_override_current_timestamptz('2018-01-02 12:00:00 +00'::timestamptz); + tsl_override_current_timestamptz +---------------------------------- + +(1 row) + +SELECT count(*) FROM test_tz WHERE time > now(); + count +------- + 3 +(1 row) + +SELECT test.tsl_override_current_timestamptz(null); + tsl_override_current_timestamptz +---------------------------------- + +(1 row) + +RESET TIME ZONE; +DROP TABLE test_tz; +-- Check that now() is evaluated on the access node. Also check that it is evaluated +-- anew on every execution of a prepared statement. +CREATE TABLE test_now (time timestamp, v int); +SELECT create_distributed_hypertable('test_now','time', + chunk_time_interval => interval '1 hour'); +NOTICE: adding not-null constraint to column "time" + create_distributed_hypertable +------------------------------- + (32,public,test_now,t) +(1 row) + +PREPARE test_query as +SELECT count(*) FILTER (WHERE time = now()), count(*) FILTER (WHERE time < now()), + count(*) FILTER (WHERE time > now()) FROM test_now; +; +BEGIN; -- to fix the value of now(); +INSERT INTO test_now VALUES + (now(), 1), (now() + INTERVAL '1 hour', 1), + (now() + INTERVAL '2 hour', 2 ), (now() + INTERVAL '3 hour', 3); +SELECT count(*) FILTER (WHERE time = now()), count(*) FILTER (WHERE time < now()), + count(*) FILTER (WHERE time > now()) FROM test_now; + count | count | count +-------+-------+------- + 1 | 0 | 3 +(1 row) + +EXECUTE test_query; + count | count | count +-------+-------+------- + 1 | 0 | 3 +(1 row) + +-- Also test different code paths used with aggregation pushdown. +-- We can't run EXPLAIN here, because now() is different every time. But the +-- strict equality should be enough to detect if now() is being erroneously +-- evaluated on data node, where it will differ from time to time. +SELECT count(*) FROM test_now WHERE time = now(); + count +------- + 1 +(1 row) + +COMMIT; +-- now() will be different in a new transaction. +BEGIN; +SELECT count(*) FILTER (WHERE time = now()), count(*) FILTER (WHERE time < now()), + count(*) FILTER (WHERE time > now()) FROM test_now; + count | count | count +-------+-------+------- + 0 | 1 | 3 +(1 row) + +EXECUTE test_query; + count | count | count +-------+-------+------- + 0 | 1 | 3 +(1 row) + +SELECT count(*) FROM test_now WHERE time = now(); + count +------- + 0 +(1 row) + +COMMIT; +DROP TABLE test_now; +DEALLOCATE test_query; +-- Cleanup DROP DATABASE :DATA_NODE_1; DROP DATABASE :DATA_NODE_2; DROP DATABASE :DATA_NODE_3; diff --git a/tsl/test/expected/dist_query.out b/tsl/test/expected/dist_query.out index aa5453902..685bad7d8 100644 --- a/tsl/test/expected/dist_query.out +++ b/tsl/test/expected/dist_query.out @@ -33,7 +33,7 @@ SET client_min_messages TO notice; -- Please see the included NOTICE for copyright information and -- LICENSE-TIMESCALE for a copy of the license. CREATE OR REPLACE FUNCTION test.tsl_override_current_timestamptz(new_value TIMESTAMPTZ) -RETURNS VOID AS :TSL_MODULE_PATHNAME, 'ts_test_override_current_timestamptz' LANGUAGE C VOLATILE STRICT; +RETURNS VOID AS :TSL_MODULE_PATHNAME, 'ts_test_override_current_timestamptz' LANGUAGE C VOLATILE; \set DN_DBNAME_1 :TEST_DBNAME _1 \set DN_DBNAME_2 :TEST_DBNAME _2 \set DN_DBNAME_3 :TEST_DBNAME _3 diff --git a/tsl/test/sql/dist_hypertable.sql.in b/tsl/test/sql/dist_hypertable.sql.in index a7aabe9ef..69eec5b22 100644 --- a/tsl/test/sql/dist_hypertable.sql.in +++ b/tsl/test/sql/dist_hypertable.sql.in @@ -1764,6 +1764,147 @@ CREATE TABLE test (time timestamp, v int); SELECT create_distributed_hypertable('test','time'); DROP TABLE test; + +-- Test that stable functions are calculated on the access node. +-- +-- As a stable function to test, use the timestamp -> timestamptz conversion +-- that is stable because it uses the current timezone. +-- We have to be careful about `timestamp < timestamptz` comparison. From postgres +-- docs: +-- When comparing a timestamp without time zone to a timestamp with time zone, +-- the former value is assumed to be given in the time zone specified by the +-- TimeZone configuration parameter, and is rotated to UTC for comparison to +-- the latter value (which is already in UTC internally). +-- We don't want this to happen on data node, so we cast the filter value to +-- timestamp, and check that this cast happens on the access node and uses the +-- current timezone. + +SELECT test.tsl_override_current_timestamptz(null); +CREATE TABLE test_tz (time timestamp, v int); +SELECT create_distributed_hypertable('test_tz','time', + chunk_time_interval => interval '1 hour'); +INSERT INTO test_tz VALUES ('2018-01-02 12:00:00', 2), ('2018-01-02 11:00:00', 1), + ('2018-01-02 13:00:00', 3), ('2018-01-02 14:00:00', 4); + +SET TIME ZONE 'Etc/GMT'; + +SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + +-- Normal WHERE clause on baserel +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + +EXPLAIN (verbose, costs off) +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + +-- Also test different code paths used with aggregation pushdown. +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + +EXPLAIN (verbose, costs off) +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + +-- TODO: test HAVING here and in the later now() tests as well. + +-- Change the timezone and check that the conversion is applied correctly. +SET TIME ZONE 'Etc/GMT+1'; +SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + +EXPLAIN (verbose, costs off) +SELECT * FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + +EXPLAIN (verbose, costs off) +SELECT count(*) FROM test_tz WHERE time > '2018-01-02 12:00:00 +00'::timestamptz::timestamp; + +-- Conversion to timestamptz cannot be evaluated at the access node, because the +-- argument is a column reference. +SELECT count(*) FROM test_tz WHERE time::timestamptz > now(); + +-- According to our docs, JIT is not recommended for use on access node in +-- multi-node environment. Turn it off so that it doesn't ruin EXPLAIN for the +-- next query. +SET jit = 0; + +-- Test that operators are evaluated as well. Comparison of timestamp with +-- timestamptz is a stable operator, and comparison of two timestamps is an +-- immutable operator. This also test that immutable functions using these +-- operators as arguments are evaluated. +EXPLAIN (verbose, costs off) +WITH dummy AS (SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp x) +SELECT * FROM test_tz, dummy +WHERE time > x + + (x = x)::int -- stable + * (x = '2018-01-02 11:00:00'::timestamp)::int -- immutable + * INTERVAL '1 hour'; + +-- Reference value for the above test. +WITH dummy AS (SELECT '2018-01-02 12:00:00 +00'::timestamptz::timestamp x) +SELECT x + (x = x)::int * (x = '2018-01-02 11:00:00'::timestamp)::int * INTERVAL '1 hour' +FROM dummy; + + +-- Check that the test function for partly overriding now() works. It's very +-- hacky and only has effect when we estimate some costs or evaluate sTABLE +-- functions in quals on access node, and has no effect in other cases. +-- Consider deleting it altogether. +SELECT test.tsl_override_current_timestamptz('2018-01-02 12:00:00 +00'::timestamptz); +SELECT count(*) FROM test_tz WHERE time > now(); +SELECT test.tsl_override_current_timestamptz(null); + +RESET TIME ZONE; +DROP TABLE test_tz; + +-- Check that now() is evaluated on the access node. Also check that it is evaluated +-- anew on every execution of a prepared statement. +CREATE TABLE test_now (time timestamp, v int); + +SELECT create_distributed_hypertable('test_now','time', + chunk_time_interval => interval '1 hour'); + +PREPARE test_query as +SELECT count(*) FILTER (WHERE time = now()), count(*) FILTER (WHERE time < now()), + count(*) FILTER (WHERE time > now()) FROM test_now; +; + +BEGIN; -- to fix the value of now(); + +INSERT INTO test_now VALUES + (now(), 1), (now() + INTERVAL '1 hour', 1), + (now() + INTERVAL '2 hour', 2 ), (now() + INTERVAL '3 hour', 3); + +SELECT count(*) FILTER (WHERE time = now()), count(*) FILTER (WHERE time < now()), + count(*) FILTER (WHERE time > now()) FROM test_now; + +EXECUTE test_query; + +-- Also test different code paths used with aggregation pushdown. +-- We can't run EXPLAIN here, because now() is different every time. But the +-- strict equality should be enough to detect if now() is being erroneously +-- evaluated on data node, where it will differ from time to time. +SELECT count(*) FROM test_now WHERE time = now(); + +COMMIT; + +-- now() will be different in a new transaction. +BEGIN; + +SELECT count(*) FILTER (WHERE time = now()), count(*) FILTER (WHERE time < now()), + count(*) FILTER (WHERE time > now()) FROM test_now; + +EXECUTE test_query; + +SELECT count(*) FROM test_now WHERE time = now(); + +COMMIT; + +DROP TABLE test_now; + +DEALLOCATE test_query; + + +-- Cleanup DROP DATABASE :DATA_NODE_1; DROP DATABASE :DATA_NODE_2; DROP DATABASE :DATA_NODE_3; diff --git a/tsl/test/sql/include/debugsupport.sql b/tsl/test/sql/include/debugsupport.sql index 1e7662f23..e5e121a87 100644 --- a/tsl/test/sql/include/debugsupport.sql +++ b/tsl/test/sql/include/debugsupport.sql @@ -3,4 +3,4 @@ -- LICENSE-TIMESCALE for a copy of the license. CREATE OR REPLACE FUNCTION test.tsl_override_current_timestamptz(new_value TIMESTAMPTZ) -RETURNS VOID AS :TSL_MODULE_PATHNAME, 'ts_test_override_current_timestamptz' LANGUAGE C VOLATILE STRICT; +RETURNS VOID AS :TSL_MODULE_PATHNAME, 'ts_test_override_current_timestamptz' LANGUAGE C VOLATILE; diff --git a/tsl/test/src/remote/scan_exec_debug.c b/tsl/test/src/remote/scan_exec_debug.c index 58c0c21fc..a610e0cb4 100644 --- a/tsl/test/src/remote/scan_exec_debug.c +++ b/tsl/test/src/remote/scan_exec_debug.c @@ -14,7 +14,14 @@ Datum ts_test_override_current_timestamptz(PG_FUNCTION_ARGS) { #ifdef TS_DEBUG - fdw_scan_debug_override_current_timestamp(PG_GETARG_INT64(0)); + if (PG_ARGISNULL(0)) + { + fdw_scan_debug_override_current_timestamp(-1); + } + else + { + fdw_scan_debug_override_current_timestamp(PG_GETARG_INT64(0)); + } PG_RETURN_VOID(); #else elog(ERROR, "unable to handle ts_test_is_frontend_session without TS_DEBUG flag set");