Skip to content

Conversation

@fandrieu
Copy link
Contributor

This is an attempt to fix bug #74243, by providing an option to add milliseconds to the date format.

Date formating is still based on dbdatecrack + printf, but the millisecond value returned by the driver is appended to the list of arguments.
The option appends ".%3d" to the printf format. (which is now a variable stored with the db handle)

With the solution we can't specify a custom date format, but it at least provides a way to get all the data and doesn't significantly change the date handling code. The format is still compatible with "new DateTime()".

@adambaratz
Copy link
Contributor

Why not add an option to let people pass in a format string? A general solution seems like it would be better. There's a ticket from others asking for a different kind of tweak.

This adds a new attribute PDO::DBLIB_ATTR_MILLISECOND
that changes the date format to include milliseconds.

Date formating is still based on dbdatecrack + printf,
but the millisecond value returned by the driver is
appended to the list of arguments.
The option appends ".%3d" to the printf format,
which is now stored with the db handle.
This adds a new attribute PDO::DBLIB_ATTR_MILLISECOND
that changes the date format to include milliseconds.

The string format now includes milliseconds,
if the option is not present the resulting string
is truncated to remove them.
Switch to slprintf as we're dealing with fixed length data
This option controls how datetime data is returned.
It has multiple purposes:
 - "raw" = converted by dblib
 - "" (default) = former behavior = ISO
 - "millisecond" = same + millisecond
 - "object" = same + wrapped in DateTime object
 - anything else = same + DateTime::format(datetime_format)
@fandrieu
Copy link
Contributor Author

I was aiming for the simpler less intrusive answer here, in my opinion that's enough fo fix most problems (as the string returned is compatible with DateTime, other conversions can be handled in php).

But I agree with the ticket you mention, having the underlying driver handle the formatting is also an elegant solution and should be an option.

Anyway, for the sake of conversation, I just pushed a patch on the other and of the spectrum: an overkill solution with a "datetime_format" string option that implements several behaviors:

  • "raw" = converted by dblib (Fix #72141)
  • "" (default) = former behavior = ISO (backward compatible)
  • "millisecond" = same + milliseconds (Fix #74243)
  • "object" = same + wrapped in DateTime object (could be done in php)
  • anything else = same + DateTime::format(datetime_format) (could be done in php)

@adambaratz
Copy link
Contributor

Before you get too deep into implementing this, let's talk through the spec and make sure we agree on that.

I don't like the idea of setting an attribute to either format strings or sentinel values. That kind of design is hard to future-proof, which is very important with libraries that someone will want to update eventually.

I'm really wary of letting this extension return DateTime objects. I don't know much about that (pretty pesky) world. I don't want create potentially unreliable code that could take a while to stabilize, would be hard to future-proof.

My suggestion would be to add an attribute that lets you set a format string. When unset, the existing behavior would be used. I don't know what control characters should be used. If we imitate the date function, that would make things awkward for the people who want to use the values from locales.conf. You could contact the people on that ticket if you want to get some more opinions.

@fandrieu
Copy link
Contributor Author

Thanks for taking the time to look into this.

I too don't like the idea of a string attribute and introducting php_date into the mix, dbconvert already provides a way to specify a custom format in locales.conf and that's what #72141 is all about.

Also the current behavior must be kept and I think it has its uses (it ensures a consistent output format, compatible with strtotime & date_create), it just lacks milliseconds as reported in #74243.

So this last commit is a more sensible solution, the attribute is now an integer with 3 possible values, to keep the current behavior and add these 2 new options:

  • DBLIB_DATETIME_FORMAT_DEFAULT = converted to YYYY-MM-DD hh:mm:ss
  • DBLIB_DATETIME_FORMAT_MILLISECOND = converted to YYYY-MM-DD hh:mm:ss.sss
  • DBLIB_DATETIME_FORMAT_CONVERT = converted by dblib (freetds uses locales.conf "date format")

@fandrieu fandrieu force-pushed the bug-74243 branch 3 times, most recently from bb682e0 to d9c5afc Compare October 19, 2017 08:53
This adds a DBLIB_ATTR_DATETIME_FORMAT attribute to control how
datetime data is converted to string.

It implements 3 behaviors:
 - DBLIB_DATETIME_FORMAT_DEFAULT = converted to YYYY-MM-DD hh:mm:ss
 - DBLIB_DATETIME_FORMAT_MILLISECOND = converted to YYYY-MM-DD hh:mm:ss.sss
 - DBLIB_DATETIME_FORMAT_CONVERT = converted by dblib
@krakjoe
Copy link
Member

krakjoe commented Oct 19, 2017

@adambaratz can we have a re-read of the latest patch and a thumbs up or down for this please ?

@adambaratz
Copy link
Contributor

I don't like the idea of having a second format built in (DBLIB_DATETIME_FORMAT_MILLISECOND). I think it would encourage people to submit patches to add presets for any variation they might want. Could we replace that option with a generic one that lets you pass in a format string? It would be even better if we could scrap that and require everyone to funnel format strings through locales.conf, but I could understand that not every person using pdo_dblib would be able to edit that file.

This adds an attribute to convert datetime data using dblib's dbconvert.
@fandrieu
Copy link
Contributor Author

fandrieu commented Oct 23, 2017

I see your point in having a second harcoded format, it should either be scraped or replaced with a customizable format.
This can't be done using the current printf approach, but we could use strftime instead, providing a php native alternative to locales.conf.
(one thing though: FreeTDS has a strftime hack to enable milliseconds via %z, this should be ported over...)

So here's some more "pick and choose" commits with that in mind, now with two different options:

  • PDO::DBLIB_ATTR_DATETIME_CONVERT (bool): converted by dblib (like mssql.datetime_convert, for #72141)
  • PDO::DBLIB_ATTR_DATETIME_FORMAT (string): converted by php using strftime + FreeTDS ms hack (for #74243)

The first option alone should be enough to satisfy most users, but the second is driver independent and doesn't require access to locales.conf.

@fandrieu fandrieu force-pushed the bug-74243 branch 2 times, most recently from 5a9e34d to 41124ce Compare October 24, 2017 15:20
This adds two attributes to convert datetime data:
 - DBLIB_DATETIME_FORMAT_CONVERT: converted by dblib
 - DBLIB_DATETIME_FORMAT_FORMAT: converted by php using strftime
This adds two attributes to convert datetime data:
 - DBLIB_DATETIME_FORMAT_CONVERT: converted by dblib
 - DBLIB_DATETIME_FORMAT_FORMAT: converted by php using strftime,
   with FreeTDS %z strftime hack.
H->stringify_uniqueidentifier = 0;
H->skip_empty_rowsets = 0;
H->datetime_convert = 0;
H->datetime_format = zend_string_init("", 0, 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default to NULL so you don't do a needless alloc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, datetime_format is now NULL unless an non-zero-length string is passed


static int pdo_dblib_strftime(zval *output, zend_string *format, DBDATEREC *dr)
{
/* mashup of php_date.c php_strftime & FreeTDS convert.c tds_strftime */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you feel about leaning on ext/date/php_date.c:php_format_date instead? It's designed to be accessible across extensions. The format strings are different, but it'd provide the same functionality (it's what powers the date function).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's how I first tried to implement this, but there were a few drawbacks:

  • milliseconds are only supported when using the DateTime object interface (like in this early commit)
  • it introduces an unneeded back and forth conversion to unix timestamp (need to call php_mktime before php_format_date)

H->stringify_uniqueidentifier = pdo_attr_lval(driver_options, PDO_DBLIB_ATTR_STRINGIFY_UNIQUEIDENTIFIER, 0);
H->skip_empty_rowsets = pdo_attr_lval(driver_options, PDO_DBLIB_ATTR_SKIP_EMPTY_ROWSETS, 0);
H->datetime_convert = pdo_attr_lval(driver_options, PDO_DBLIB_ATTR_DATETIME_CONVERT, 0);
H->datetime_format = pdo_attr_strval(driver_options, PDO_DBLIB_ATTR_DATETIME_FORMAT, 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use NULL instead of 0 for the default value.

This adds two attributes to convert datetime data:
 - DBLIB_DATETIME_FORMAT_CONVERT: converted by dblib
 - DBLIB_DATETIME_FORMAT_FORMAT: converted by php using date(),
   with "u"/"f" format hack to support micro/milli seconds
@fandrieu
Copy link
Contributor Author

So here's a more php-y DATETIME_FORMAT implementation, based on php_format_date :)

I actually preferred this one and have now figured out a decent way of putting it together:

  • convert to timestamp with a single dbconvert call, using the bigdatetime format
  • hack milli (and micro) second support in date(), using the "inject in format" approach from FreeTDS

Convert to SYBMSDATETIME2 to get timestamp:
 - works with FreeTDS < 1
 - better precision
@fandrieu
Copy link
Contributor Author

I fixed this last commit:

  • use DATETIME2 as intermediary format: it works with FreeTDS<1 and offers better precision
  • support escaping in date() format: I missed that

@fandrieu fandrieu force-pushed the bug-74243 branch 5 times, most recently from 46a772b to 1322cf1 Compare October 27, 2017 07:59
@adambaratz
Copy link
Contributor

I know I suggested going in this direction, but I'm getting uncomfortable with the amount of (partially copied, pasted, and rearranged) code involved here. It feels like more business logic than should be living in a driver layer. I'm willing to be okay with it if this is the only way to make it work, but... I'm wondering how true that is.

The CAST function has some date/time options. If we only add the DBLIB_DATETIME_FORMAT_CONVERT attribute and ask everyone else to CAST as needed (or use other functions), will there be any use cases left out? There are a few options available through CAST for getting the milliseconds out.

@fandrieu
Copy link
Contributor Author

I agree, DATETIME_CONVERT is enough and the code I came up for DATETIME_FORMAT is too complicated and doesn't add much (basically an option for people without access to locales.conf).

Anyway I'm OK with scratching this last part and focusing on DATETIME_CONVERT, I pushed a commit doing that.

One last thing though: I'd like to include SQLDATETIME2 in the switch case (like in this commit), to get a consistent behavior for datetime & datetime2 (as of now datetime2 isn't supported and falls back to the default behavior)

This adds an attribute to convert datetime data using dblib's dbconvert.
@php-pulls
Copy link

Comment on behalf of adambaratz at php.net:

Committed b72af30.

@adambaratz
Copy link
Contributor

This change should be all set. Thanks for all your work on it. I'll take a look at merging the SQLDATETIME2 change separately.

@fandrieu
Copy link
Contributor Author

fandrieu commented Nov 3, 2017

Thanks for working on this, glad this change made it.

Just a thought: the allocation for the generic dbconvert may not be enough.
tmp_data_len = 32 + (2 * (data_len)) is 48 for datetime and won't accomodate the 63 chars FreeTDS allows for formated dates. (such long date strings may never happen, just wanted to mention it)

@adambaratz
Copy link
Contributor

Where did you get the 63 for the length? The closest thing I could find in the FreeTDS source was types.csv, which had very different values.

@fandrieu
Copy link
Contributor Author

fandrieu commented Nov 7, 2017

I did some tests with a large date format and found that the resulting string is truncated to 63 chars.
You can see it in the code here.

With a large enough buffer, datetime_convert.phpt would output something like:
string(63) "Nov 7 2017 09:19:34:570AM PAD_PAD_PAD_PAD_PAD_PAD_PAD_PAD_PAD_"
(without you could get a Termsig=11)

@fandrieu
Copy link
Contributor Author

fandrieu commented Nov 7, 2017

PS: this test can prevent the bug, it could for example be implemented here like this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants