I have a table called providers where the column notice_period takes values such as interval '1 month' - interval '2 days'. If I do the following:
select to_date('11.11.2020', 'dd-mm-yyyyy') + notice_period from providers
I get the following error:
42883] ERROR: operator does not exist: ` text
How can I avoid this error and use the value of notice_period to directly calculate a valid date?
Cheers
Postgres readily converts intervals to strings. I would recommend removing the interval key word and just using ::interval:
select date_trunc('year', now()) + notice_period::interval
from (values ('1 day'), ( '3 month 1 day')) v(notice_period)
If you insist on the interval in the string, then remove it:
+ replace(notice_period, 'interval ', '')::interval
or:
+ substr(notice_period, 10)::interval
Another option is to use dynamic SQL in a function:
CREATE FUNCTION add_period(timestamp with time zone) RETURNS timestamp with time zone
LANGUAGE plpgsql STRICT AS
$$DECLARE
result timestamp with time zone;
BEGIN
EXECUTE format(
'SELECT TIMESTAMP WITH TIME ZONE %L + %s',
$1,
(SELECT notice_period FROM providers)
) INTO result;
RETURN result;
END;$$;
The problem with that is that the whole thing is vulnerable to SQL injection by bad values in the table providers, so use that only if you can trust the source.
This problem cannot be avoided with SQL strings supplied from outside.
Related
I am attempting to add a day to NOW() and return as the values for a column.
This works
SELECT NOW() as date
But this gives an error
SELECT DATE_ADD( NOW(), INTERVAL 1 DAY) as date
Is there a way to achieve this in a postgres query?
Thanks
I don't think there's a date_add() function in PostgreSQL:
ERROR: function date_add(timestamp with time zone, interval) does not
exist
LINE 1: select date_add(now(), interval '1 day');
^
HINT: No function matches the given name and argument types. You
might need to add explicit type casts.
but you can use a regular + operator to add an interval to timestamptz that's returned by now(). Demo:
select now() + '1 day'::interval;
You can define that function for convenience:
create function date_add(arg1 timestamptz, arg2 interval)
returns timestamptz language sql as $$
select arg1+arg2
$$;
select date_add(now(), interval '1 day') as date;
-- date
---------------------------------
-- 2022-11-29 12:28:12.393508+00
But I don't think it's really more convenient than the operator. You'd also have to overload it to make sure how it deals with different types - you can see in the demo how by default PostgreSQL will try to guess and cast automatically.
I have a procedure with an int parameter.
CREATE OR REPLACE PROCEDURE update_retention_policy(id int, days int)
language plpgsql
AS
$$
BEGIN
PERFORM add_retention_policy(('schema_' + id + '.my_hypertable'), days * INTERVAL '1 day', true);
END
$$;
The syntax for the add_retention_policy function is add_retention_policy('hypertable', INTERVAL 'x days', true). I want to prefix the hypertable with the schema which is always 'schema_' and then followed by the id parameter, how do I do that?
You just need to rewrite the INTERVAL part in your function call as days * INTERVAL '1 day'.
Instead of concatenating strings, you multiply the '1 day' interval by the days param.
EDIT: for the id part, you can just use the || operator, which is the string concatenation operator in Postgres, instead of +. You shouldn't even need to explicitly cast id to character varying
I have a query like the following
CAST(kokyaku1Information2.mail_jyushin as integer) as information2_mail_jyushin,
(date '$mytime' - INTERVAL 'information2_mail_jyushin' day) AS modified_date,
When run the query i get an error like 'invalid input syntax for type interval'. I used another select field named information2_mail_jyushin before day.
In Postgres, you would use interval arithmetics like this:
kyaku1Information2.mail_jyushin::int AS information2_mail_jyushin,
date '$mytime'
- kokyaku1Information2.mail_jyushin::int * interval '1 day'
AS modified_date
Note that concatenating variables in a SQL statement is bad practice, and opens up your code to SQL injection attacks. Instead, use parameters, as in:
$1::date
- kokyaku1Information2.mail_jyushin::int * interval '1 day'
AS modified_date
Argument days in function getAvgByDay() doesn't work, I guess because it is inside quotes:
CREATE OR REPLACE FUNCTION getAvgByDay(days int)
RETURNS TABLE ( average text,
date timestamp with time zone
) AS
$func$
BEGIN
RETURN QUERY
SELECT to_char( AVG(measure), '99999D22') AS average, ( now() - interval '$1 day') AS date
FROM (
SELECT mes.date, mes.measure
FROM measures mes
WHERE mes.date < ( now() - interval '$1 day')
) AS mydata;
END
$func$
LANGUAGE plpgsql;
Assuming the column measures.date is actually data type timestamptz and not a date:
CREATE OR REPLACE FUNCTION get_avg_by_day(_days int)
RETURNS TABLE (average text, ts timestamptz) AS -- not using "date" for a timestamp
$func$
SELECT to_char(avg(measure), '99999D22') -- AS average
, now() - interval '1 day' * $1 -- AS ts
FROM measures m
WHERE m.date < now() - interval '1 day' * $1
$func$ LANGUAGE sql;
No need for PLpgSQL, can be a simper SQL function.
Difference between language sql and language plpgsql in PostgreSQL functions
No need for a subquery. Only adds complexity and cost for no gain.
No need for column aliases in the outer query level. Those are not used, as visible column names are defined in the RETURNS clause.
No need for extra parentheses. Operator precedence works as desired anyway. (No harm in this case, either.)
Don't use CaMeL case identifier in Postgres if you can avoid it.
Are PostgreSQL column names case-sensitive?
Don't call a timestamptz column "date". That's misleading. Using "ts" instead.
Most importantly: You suspected as much, and "sticky bit" already explained: no interpolation inside strings. But just multiply the time unit with your integer input to subtract the given number of days:
interval '1 day' * $1
That's faster and cleaner than string concatenation.
There's no interpolation in strings. But you can concatenate strings and cast them to an interval. Try:
... concat(days, ' day')::interval ...
Or you could use format(), that's probably a little closer to what you originally had:
... format('%s day', days)::interval ...
I am trying to use a parameterized query in a Perl script to get some timestamps back from a Postgres database. Here's a cut-and-dried example, solely for pedagogical purposes.
I've defined $start_date and $end_date as timestamps and intervals:
my $start_date = "current_timestamp - interval '6 hours'";
my $end_date = "current_timestamp";
I use the following to submit to the database, with $dbh defined earlier:
my $sql = "SELECT cast(? as timestamp), cast(? as timestamp)";
my $sth = $dbh->prepare($sql);
$sth->execute($start_date, $end_date);
When I do this, I get a somewhat confusing error.
DBD::Pg::st execute failed: ERROR: date/time value "current" is no longer supported
I understand that current hasn't been supported in PG since 7.2, but I'm not using that. I'm using current_timestamp, which is supported, AFACT. To wit, if I enter into psql:
select (cast(current_timestamp - interval '6 hours' as timestamp), cast(current_timestamp as timestamp));
the result is what I expect (two timestamps, the former six hours previous to the latter).
I could also use now() rather than current_timestamp. I can use it in the following way:
my $start_date = "now() - interval '6 hours'";
my $end_date = "now()";
When I try to run the query in perl, I get the following error:
DBD::Pg::st execute failed: ERROR: invalid input syntax for type timestamp: "now() - interval '6 hours'"
Yet, the query:
select (cast(now() - interval '6 hours' as timestamp), cast(now() as timestamp));
gives me the expected result.
I am quite flummoxed.
The problem is that a SQL placeholder doesn't represent an expression, but a single value. And that value can't be a function. You could do something like:
my $start_date = "6 hours";
my $sql = "SELECT current_timestamp - cast(? as interval), current_timestamp";
my $sth = $dbh->prepare($sql);
$sth->execute($start_date);
What you're doing in Perl is equivalent to doing this in psql:
select (cast('current_timestamp - interval ''6 hours''' as timestamp), cast('current_timestamp' as timestamp));
To make the windows of your queries a bit more flexible:
$sth = $dbh->prepare(<<__eosql);
SELECT * FROM tbl
WHERE ts BETWEEN current_timestamp - ? * CAST('1 ' || ? AS INTERVAL)
AND
current_timestamp;
__eosql
$sth->execute(6, 'hour');
$sth->execute(10, 'day');
$sth->execute(1, 'week');
# etc.
When you introduce fixed time points, you could do something too clever like ... WHERE COALESCE(?, current_timestamp) ... and remember that an undef parameter defaults to the current time. However, I'd probably write and prepare a separate query.