f3-cortex icon indicating copy to clipboard operation
f3-cortex copied to clipboard

Computing time 'INTERVAL' with postgres

Open kumy opened this issue 5 years ago • 14 comments
trafficstars

I'm having troubles selecting rows based on datetime calculation on a Postgres database.

The query I'm trying to build looks like:

SELECT *
FROM "news"
WHERE "created_on_datetime" > NOW()- INTERVAL '1 YEAR'
ORDER BY "created_on_datetime" DESC;

A simple code example to reproduce could be:

        $newsModel = new News();
        $filter = ['created_on_datetime > NOW() - INTERVAL \'1 YEAR\''];
        $news = $newsModel->find($filter);

From the debug lines I've added to the cortex code, the generated query is:

SELECT "id","title","content","author","created_on_datetime" FROM "news" WHERE "created_on_datetime" > NOW() - "INTERVAL" '1 YEAR'

Note the double quotes added automatically around the INTERVAL keyword. (Without them the query works fine. Those leads to the Postgres error:

Error in query (7): ERROR: type "INTERVAL" does not exist
LINE 1: ...M "news" WHERE "created_on_datetime" > NOW() - "INTERVAL"...

I've tracked the quotes adder down to the function sql_quoteCondition() https://github.com/ikkez/f3-cortex/blob/364caaa15132824a26ee0207e35b70750c50e5ae/lib/db/cortex.php#L2683-L2706

Here is a regex101 fork, were we clearly see the INTERVAL captured as a column name. https://regex101.com/r/AP2mRH/1

I'm on:

/**
 *  Cortex - the flexible data mapper for the PHP Fat-Free Framework
 *  […]
 *  @package DB
 *  @version 1.6.0
 *  @date 03.02.2019
 *  @since 24.04.2012
 */

I hope this one will not give you more sleepless nights :wink:

kumy avatar Mar 30 '20 18:03 kumy

A workaround could be to write the filter as:

        $filter = ['created_on_datetime > NOW() - \'1 YEAR\'::interval'];

But I still have to find how to properly escape it in Pdo when it's parametrized:

        $filter = ['created_on_datetime > NOW() - \'? YEAR\'::interval', $years];

This give error:

Named bind parameter `:interval` does not exist in filter arguments

Other tries:

        $filter = ['created_on_datetime > NOW() - cast(? as interval)', $year.' YEAR'];  // OK, but I would like to avoid concat
        $filter = ['created_on_datetime > NOW() - cast(\'? DAY\' as interval)', $year]; // FAIL
        $filter = ['created_on_datetime > NOW() - cast(concat(?::integer, \' DAY\') as interval)', $year]; // Fail
        $filter = ['created_on_datetime > NOW() - cast(concat(cast(?) as integer, \' DAY\') as interval)', $year]; // FAIL
        $filter = ['created_on_datetime > NOW() - cast(concat(?, \' DAY\') as interval)', $year]; // FAIL

kumy avatar Mar 30 '20 18:03 kumy

use the Date method to encapsulate the expression in a function, that'll skip the qoutation of parameters and fields inside. See duplicate: https://github.com/ikkez/f3-cortex/issues/28#issuecomment-219035624

ikkez avatar Apr 01 '20 12:04 ikkez

Thanks for your answer, is DATE() an internal cortex keyword? I can't find it in Postgres doc https://www.postgresql.org/docs/9.1/functions-datetime.html

kumy avatar Apr 01 '20 12:04 kumy

it's a SQL function as far as i know

ikkez avatar Apr 01 '20 13:04 ikkez

The best I can get is:

        $filter = ['created_on_datetime > NOW() - cast(? as interval)', $year.' DAY'];
        $filter = ['created_on_datetime > DATE(NOW() - INTERVAL ?)', $year.' YEAR']; // FAIL
// PDOStatement: ERROR: syntax error at or near "$1" LINE 1: ...ERE "created_on_datetime" > DATE(NOW() - INTERVAL $1) ORDER ... ^

        $filter = ['created_on_datetime > DATE(NOW() - INTERVAL \'? YEAR\')', $year]; // FAIL
// PDOStatement: ERROR: invalid input syntax for type interval: "? YEAR" LINE 1: ...ERE "created_on_datetime" > DATE(NOW() - INTERVAL '? YEAR') ... ^

kumy avatar Apr 01 '20 13:04 kumy

seems like the DATE function is not available for postgre. Try this one for postgre:

$filter = ["created_on_datetime > date_trunc('second', INTERVAL '? YEAR')", $year];

// or begin without parameter, since I'm not sure about where to put that atm without testing
$filter = ["created_on_datetime > date_trunc('second', INTERVAL '1 YEAR')"];

ikkez avatar Apr 01 '20 13:04 ikkez

$filter = ["created_on_datetime > date_trunc('second', INTERVAL '? YEAR')", GK_SITE_NEWS_DISPLAY_DAYS_VALIDITY];
// PDOStatement: ERROR: invalid input syntax for type interval: "? YEAR" LINE 1: ...ated_on_datetime" > date_trunc('second', INTERVAL '? YEAR') ... ^

$filter = ["created_on_datetime > date_trunc('second', INTERVAL '1 YEAR')"];
// PDOStatement: ERROR: operator does not exist: timestamp with time zone > interval LINE 1: ...etime" FROM "gk_news" WHERE "created_on_datetime" > date_tru... ^ HINT: No operator matches the given name and argument types. You might need to add explicit type casts.

Screenshot from 2020-04-01 15-48-43

kumy avatar Apr 01 '20 13:04 kumy

That's good! See the hint:

HINT: No operator matches the given name and argument types. You might need to add explicit type casts.

I'm not sure what type your created_on_datetime field is, so maybe you just need to cast the one or the other field to aid in comparision.. the error states that he cannot compare a timestamp incl. timezone with whatever your other field is

ikkez avatar Apr 01 '20 13:04 ikkez

Despite of this workaround.. it seem more complicated in postgre than other engine.. maybe i should just add an option somehow to display automatic field quotation for specific parts of the query?!

ikkez avatar Apr 01 '20 13:04 ikkez

We missed the now() - in the last tests:

        $filter = ["created_on_datetime > NOW() - date_trunc('second', INTERVAL '1 YEAR')"];
// Works, but not with '? YEAR' - seems related to Pdo

        $filter = ["created_on_datetime > NOW() - date_trunc('second', INTERVAL '? YEAR')", GK_SITE_NEWS_DISPLAY_DAYS_VALIDITY];
// PDOStatement: ERROR: invalid input syntax for type interval: "? YEAR" LINE 1: ...datetime" > NOW() - date_trunc('second', INTERVAL '? YEAR') ... ^

My best option right now still seems to be

$filter = ['created_on_datetime > NOW() - cast(? as interval)', GK_SITE_NEWS_DISPLAY_DAYS_VALIDITY.' DAY'];

How would the option be used, would it be a list of keywords to not quote? Migration to Postgres was a huge work for me :disappointed: with lots of gotchas

kumy avatar Apr 01 '20 14:04 kumy

Could it be that the NOW() need to be set within the function? created_on_datetime > date_trunc('second', NOW() - INTERVAL '1 YEAR')

ikkez avatar Apr 01 '20 15:04 ikkez

@ikkez thanks for yor help.

We're getting quite close to something "usable"/"clean".

This syntax may work with a little update to the library.

        $filter = ["created_on_datetime > NOW() - CAST(? || ' YEAR' as INTERVAL)", GK_SITE_NEWS_DISPLAY_DAYS_VALIDITY];

But problem here is that cortex parse it as Mysql syntax, which has completely different meaning with Postgres. "PostgreSQL, following the standard, uses || for string concatenation ('foo' || 'bar' = 'foobar')."

MySQL uses C-language operators for logic (i.e. 'foo' || 'bar' means 'foo' OR 'bar', 'foo' && 'bar' means 'foo' and 'bar'). This might be marginally helpful for C programmers, but violates database standards and rules in a significant way. PostgreSQL, following the standard, uses || for string concatenation ('foo' || 'bar' = 'foobar'). https://wiki.postgresql.org/wiki/Things_to_find_out_about_when_moving_from_MySQL_to_PostgreSQL

Problem is there: https://github.com/ikkez/f3-cortex/blob/364caaa15132824a26ee0207e35b70750c50e5ae/lib/db/cortex.php#L2585

This substitution have to be conditioned to the driver used.

I've tried to workaround it without updating cortex, but it finally gets more complicated, with no luck and finally we're back to the same problem of quotes:

        $filter = ["created_on_datetime > NOW() - CAST(CONCAT(?, ' YEAR') AS INTERVAL)", GK_SITE_NEWS_DISPLAY_DAYS_VALIDITY];
// PDOStatement: ERROR: could not determine data type of parameter $1

        $filter = ["created_on_datetime > NOW() - CAST(CONCAT(CAST(? AS INTEGER), ' YEAR') AS INTERVAL)", GK_SITE_NEWS_DISPLAY_DAYS_VALIDITY];
// PDOStatement: ERROR: syntax error at or near ""AS"" LINE 1: ...NOW() - CAST(CONCAT(CAST($1 AS INTEGER), ' YEAR') "AS" "INTE... ^

I start wondering why this auto quoting is there, what are the use case behind it?

And sorry I forgot to share the table schema: Screenshot from 2020-04-01 19-09-00

kumy avatar Apr 01 '20 17:04 kumy

Oh! I may simply follow the doc! I'll test that…

CORTEX.quoteConditions: Default TRUE. By default, all field names in where conditions are quoted automatically according to the used database engine. This helps to work around reserved names in SQL. However the detection of fields isn't perfect yet, so in case you want to add the correct backticks or other quotation yourself, set this to FALSE. https://github.com/ikkez/f3-cortex#additional-notes

Edit: OK it fix the example from my first post, but to have it dynamic, bind only one int, "created_on_datetime > NOW() - CAST(? || ' YEAR' as INTERVAL)" is the only way I found, but the OR vs || bug is annoying there. If we fix it, then this issue could be closed.

@ikkez Would you a dedicated issue for the OR bug? Edit2 : :arrow_right: #98

kumy avatar Apr 01 '20 17:04 kumy

An approach is to use plpgsql to create my own function:

CREATE OR REPLACE FUNCTION public.fresher_than(
	datetime timestamp with time zone,
	duration integer,
	unit character varying)
    RETURNS boolean
    LANGUAGE 'plpgsql'

AS $BODY$BEGIN
	RETURN datetime > NOW() - CAST(duration || ' ' || unit as INTERVAL);
END;$BODY$;

And use it as:

$filter = ["fresher_than(created_on_datetime, ?, 'YEAR')", $years];

This is fine for me, @ikkez you may close the issue if you wish :+1:

Note: a view would probably had success there too.

kumy avatar Apr 01 '20 19:04 kumy