ADODB无法存储带有亚秒精度的DATETIME值。

3
根据Microsoft关于DATETIME列类型的文档,该类型的值可以存储“准确到.000、.003或.007秒的增量精度”。根据他们ADODB使用的数据类型的文档,adDBTimeStamp(代码135)是ADODB用于DATETIME列参数的类型,“表示日期/时间戳(yyyymmddhhmmss加上十亿分之一的小数部分)”。然而,当传递带有亚秒精度的参数时,所有尝试都失败了(使用多个版本的SQL Server以及SQLOLEDB提供程序和更新的SQLNCLI11提供程序进行测试)。以下是演示故障的重现案例:
import win32com.client

# Connect to the database
conn_string = "Provider=...." # sensitive information redacted
conn = win32com.client.Dispatch("ADODB.Connection")
conn.Open(conn_string)

# Create the temporary test table
cmd = win32com.client.Dispatch("ADODB.Command")
cmd.ActiveConnection = conn
cmd.CommandText = "CREATE TABLE #t (dt DATETIME NOT NULL)"
cmd.CommandType = 1 # adCmdText
cmd.Execute()

# Insert a row into the table (with whole second precision)
cmd = win32com.client.Dispatch("ADODB.Command")
cmd.ActiveConnection = conn
cmd.CommandText = "INSERT INTO #t VALUES (?)"
cmd.CommandType = 1 # adCmdText
params = cmd.Parameters
param = params.Item(0)
print("param type is {:d}".format(param.Type)) # 135 (adDBTimeStamp)
param.Value = "2018-01-01 12:34:56"
cmd.Execute() # this invocation succeeds

# Show the result
cmd = win32com.client.Dispatch("ADODB.Command")
cmd.ActiveConnection = conn
cmd.CommandText = "SELECT * FROM #t"
cmd.CommandType = 1 # adCmdText
rs, rowcount = cmd.Execute()
data = rs.GetRows(1)
print(data[0][0]) # displays the datetime value stored above

# Insert a second row into the table (with sub-second precision)
cmd = win32com.client.Dispatch("ADODB.Command")
cmd.ActiveConnection = conn
cmd.CommandText = "INSERT INTO #t VALUES (?)"
cmd.CommandType = 1 # adCmdText
params = cmd.Parameters
param = params.Item(0)
print("param type is {:d}".format(param.Type)) # 135 (adDBTimeStamp)
param.Value = "2018-01-01 12:34:56.003" # <- blows up here
cmd.Execute()

# Show the result
cmd = win32com.client.Dispatch("ADODB.Command")
cmd.ActiveConnection = conn
cmd.CommandText = "SELECT * FROM #t"
cmd.CommandType = 1 # adCmdText
rs, rowcount = cmd.Execute()
data = rs.GetRows(2)
print(data[0][1])

在上面指出的那一行,此代码会抛出一个异常,错误信息为“应用程序在当前操作中使用了错误类型的值。”这是ADODB中已知的一个bug吗?如果是,我还没有找到任何相关讨论。(也许早些时候有讨论,但当微软关闭KB页面时就消失了。)如果该值与文档相符,它怎么可能是错误类型的呢?


我不知道答案,但是在.Net中是否有函数可以使用数字构建日期而不是使用字符串? - Nick.Mc
@Nick.McDermaid - 我们正在使用一个Python包,所以.Net不可用。此外,第一个INSERT语句在原始帖子中已经指出,使用日期和时间的字符串表示作为参数值是正确的。 - Bob Kline
不知道。我会添加Python标签。 - Nick.Mc
@Nick.McDermaid - 这个问题涉及到ADODB(与ADO.NET不同),而不是特定于所使用的编程语言。我在使用Perl或PHP时会得到相同的行为。 - Bob Kline
也许你可以直接将一个数据放入数据库中,然后再选择它并查看是否能对此事有所启示。 - Nick.Mc
@Nick.McDermaid - 我可以插入具有亚秒精度的DATETIME值,并使用ADODB检索该值,并确认保留了亚秒精度。然而,如果我尝试通过将同一对象作为参数值传回以进行后续插入来往返这个值,那么亚秒精度将被静默丢弃。所以我们回到了最初的问题:ADODB是否无法遵守文档(保留作为参数传递的值的准确性)是一个已知的错误? - Bob Kline
1个回答

3

这是SQL Server OLEDB驱动程序中一个众所周知的漏洞,可以追溯到20多年前,这意味着它永远不会被修复。

这也不是ADO中的错误。 ActiveX数据对象(ADO)API是底层OLEDB API的薄包装器。该漏洞存在于Microsoft的SQL Server OLEDB驱动程序本身(所有版本)。他们现在绝对不会,绝对不会绝对不会修复它;因为他们太胆小了,不想维护现有代码可能会破坏现有应用程序。

因此,这个漏洞已经传承了几十年:

  • SQOLEDB(1999)SQLNCLI(2005)SQLNCLI10(2008)SQLNCLI11(2010)MSOLEDB(2012)
唯一的解决方案是,不要将datetime作为时间戳参数化,而是将其作为"ODBC 24小时格式"的字符串参数化:yyyy-mm-dd hh:mm:ss.zzz。具体来说:
  • adTimestamp(又名DBTYPE_DBTIMESTAMP135)需要使用以下类型进行参数化:
  • adChar(又名DBTYPE_STR129),例如:2021-03-21 17:51:22.619

或者使用ADO特定的字符串类型:

  • adVarChar200),例如:2021-03-21 17:51:22.619

其他的DBTYPE_xxx类型如何处理?

你可能认为adDate(又称DBTYPE_DATE, 7) 看起来很有前途:

表示日期值(DBTYPE_DATE)。 日期存储为double类型,其整数部分是从1899年12月30日以来的天数,小数部分是一天的分数。

但不幸的是,它也将值参数化到服务器上没有毫秒:

exec sp_executesql N'SELECT @P1 AS Sample',N'@P1 datetime','2021-03-21 06:40:24'

您也不能使用adFileTime,它看起来很有前途:

表示自1601年1月1日以来的100纳秒间隔数的64位值(DBTYPE_FILETIME)。

这意味着它可以支持0.0000001秒的分辨率。

不幸的是,根据VARIANT规则,您不允许在VARIANT中存储FILETIME。由于ADO对所有值都使用变量,因此当它遇到变量类型64(VT_FILETIME)时,它会出现错误。

解码TDS以确认我们的怀疑

我们可以通过解码发送到服务器的数据包来确认SQL Server OLEDB驱动程序没有提供具有可用精度的datetime

我们可以发出批处理命令:

SELECT ? AS Sample

并指定参数1:adDBTimestamp - 3/21/2021 6:40:23.693

现在我们可以捕获这个数据包:

0000   03 01 00 7b 00 00 01 00 ff ff 0a 00 00 00 00 00   ...{............
0010   63 28 00 00 00 09 04 00 01 32 28 00 00 00 53 00   c(.......2(...S.
0020   45 00 4c 00 45 00 43 00 54 00 20 00 40 00 50 00   E.L.E.C.T. .@.P.
0030   31 00 20 00 41 00 53 00 20 00 53 00 61 00 6d 00   1. .A.S. .S.a.m.
0040   70 00 6c 00 65 00 00 00 63 18 00 00 00 09 04 00   p.l.e...c.......
0050   01 32 18 00 00 00 40 00 50 00 31 00 20 00 64 00   .2....@.P.1. .d.
0060   61 00 74 00 65 00 74 00 69 00 6d 00 65 00 00 00   a.t.e.t.i.m.e...
0070   6f 08 08 f2 ac 00 00 20 f9 6d 00                  o...... .m.

并解码它:

03                  ; Packet type. 0x03 = 3 ==> RPC
01                  ; Status
00 7b               ; Length. 0x07B ==> 123 bytes
00 00               ; SPID
01                  ; Packet ID
00                  ; Window
ff ff               ; ProcName 0xFFFF => Stored procedure number. UInt16 number to follow
0a 00               ; PROCID  0x000A ==> stored procedure ID 10 (10=sp_executesql)
00 00               ; Option flags (16 bits)

00 00 63 28 00 00 00 09   ; blah blah blah 
04 00 01 32 28 00 00 00   ; 

53 00 45 00 4c 00 45 00   ; \  
43 00 54 00 20 00 40 00   ;  |
50 00 31 00 20 00 41 00   ;  |- "SELECT @P1 AS Sample"
53 00 20 00 53 00 61 00   ;  |
6d 00 70 00 6c 00 65 00   ; /

00 00 63 18 00 00 00 09   ;  blah blah blah
04 00 01 32 18 00 00 00   ;

40 00 50 00 31 00 20 00   ; \
64 00 61 00 74 00 65 00   ;  |- "@P1 datetime"
74 00 69 00 6d 00 65 00   ; /

00 00 6f 08 08      ; blah blah blah

f2 ac 00 00         ; 0x0000ACF2 = 44,274 ==> 1/1/1900 + 44,274 days = 3/21/2021
20 f9 6d 00         ; 0x006DF920 = 7,207,200 ==> 7,207,200 / 300 seconds after midnight = 24,024.000 seconds = 6h 40m 24.000s = 6:40:24.000 AM

简短的版本是,datetime 在传输过程中被指定为以下形式:

datetime 表示如下:

  • 一个 4 字节有符号整数,表示自 1900 年 1 月 1 日以来的天数。负数可用于表示自 1753 年 1 月 1 日以来的日期。
  • 一个 4 字节无符号整数,表示从当天凌晨 12 点开始计算的三百分之一秒数(每秒 300 次)。
这意味着我们可以读取驱动程序提供的 datetime 如下:
  • 日期部分:0x0000acf2 = 44,274 = 1900 年 1 月 1 日 + 44,274 天 = 2021 年 3 月 21 日
  • 时间部分:0x006df920 = 7,207,200 = 7,207,200 / 300 秒 = 上午 6:40:24
所以驱动程序截断了我们的 datetime 的精度。
Supplied date: 2021-03-21 06:40:23.693
Date in TDS:   2021-03-21 06:40:24

换句话说:

  • OLE Automation使用Double来表示datetime

  • Double的分辨率为约0.0000003秒。

  • 驱动程序有选项将时间编码为1/300秒:

    6:40:24.6937,207,4070x006DF9EF

但它选择不这样做。错误:驱动程序。

帮助解码TDS的资源


微软在发布Linux版SQL Server后不久,是不是就废弃了OLEDB驱动程序,转而支持ODBC了? - undefined
@PanagiotisKanavos 他们确实这样做了。然后不久之后,他们取消废弃了它们,然后创建了另一个全新的OLEDB驱动程序:msoledbsql。此外,对于任何阅读此内容的人,您不应该使用Microsoft的ODBC驱动程序来访问SQL Server,因为它们存在一些可怕的无法修复的错误(https://stackoverflow.com/questions/45511013, https://stackoverflow.com/questions/38798174, http://stackoverflow.com/questions/35296182)。 - undefined
在Linux或Python数据科学脚本中,你不能使用OLEDB,只能使用ODBC。正如一些SO答案中的评论所指出的那样,最近的ODBC版本(17、18)似乎已经解决了一些问题。在2017年之前,ODBC驱动程序一直停滞不前,但是鉴于Linux和Python,大量的工作已经投入到修复这些错误上。 - undefined
我看过几个Stack Overflow上的Python问题,其中的问题是由于复制粘贴旧代码并引用了17年之前的驱动程序。只需使用17年或之后的版本就可以解决这些问题。 - undefined
@PanagiotisKanavos 是的,完全正确,我只是在提到Windows系统时,有人可能会尝试使用ODBC驱动程序而不是OLEDB驱动程序。如果您不在Windows上,那么很有可能SQL Server的ODBC驱动程序是正确地从头开始编写的。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接