ADODB无法以亚秒级精度存储DATETIME值

时间:2018-02-25 01:34:28

标签: python sql-server adodb

根据DATETIME列类型的Microsoft documentation,该类型的值可以存储"精度四舍五入到.000,.003或.007秒的增量。"根据ADODB使用的数据类型documentation,ADODB用于DATETIME列参数的adDBTimeStamp(代码135),"表示日期/时间戳(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])

此代码在上面指定的行上引发异常,并显示错误消息&#34; Application对当前操作使用了错误类型的值。&#34;这是ADODB中的已知错误吗?如果是这样,我还没有找到任何讨论。 (也许之前的讨论在微软杀死KB页面时就消失了。)如果值与文档匹配,那么该值的类型是什么?

1 个答案:

答案 0 :(得分:1)

这是 SQL Server OLEDB 驱动程序中的一个众所周知的错误,可追溯到 more than 20 years;这意味着它永远不会被修复。

这也不是 ADO 中的错误。 ActiveX 数据对象 (ADO) API 是底层 OLEDB API 的瘦包装器。该错误存在于 Microsoft 的 SQL Server OLEDB 驱动程序本身(所有这些)中。他们永远不会,永远永远现在修复它;由于它们是不想维护现有代码的鸡屎,因此可能会破坏现有应用程序。

所以这个错误已经延续了几十年:

  • SQOLEDB (1999)SQLNCLI (2005)SQLNCLI10 (2008)SQLNCLI11 (2010)MSOLEDB (2012)

唯一的解决方案是将您的 datetime 参数化为时间戳

  • adTimestamp(又名 DBTYPE_DBTIMESTAMP135

您需要将其参数化为“ODBC 24 小时格式” yyyy-mm-dd hh:mm:ss.zzz string :

  • adChar(又名 DBTYPE_STR129):2021-03-21 17:51:22.619

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

  • adVarChar (200):2021-03-21 17:51:22.619

其他 DBTYPE_xxx 呢?

您可能认为 adDate(又名 DBTYPE_DATE7looks promising:

<块引用>

表示日期值 (DBTYPE_DATE)。日期以双精度形式存储,其中整数部分是自 1899 年 12 月 30 日以来的天数,小数部分是一天的分数。

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

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

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

<块引用>

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

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

不幸的是,rules of VARIANTs 不允许您将 FILETIME 存储在 VARIANT 中。由于 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 指定为on-the-wire 为:

<块引用>

日期时间按以下顺序表示:

  • 一个 4 字节有符号整数,表示自 1900 年 1 月 1 日以来的天数。允许负数表示自 1753 年 1 月 1 日以来的日期。
  • 一个 4 字节无符号整数,表示从那天上午 12 点开始经过的三分之三秒(每秒 300 次计数)的数目。

这意味着我们可以将驱动程序提供的 datetime 读取为:

  • 日期部分:0x0000acf2 = 44,274 = 1900 年 1 月 1 日 + 44,274 天 = 3/21/2021
  • 时间部分:0x006df920 = 7,207,200 = 7,207,200 / 300 秒 = 6:40:24 AM

所以驱动程序切断了我们日期时间的精度:

Supplied date: 2021-03-21 06:40:23.693
Date in TDS:   2021-03-21 06:40:24

换句话说:

  • OLE 自动化使用 Double 来表示 datetime

  • Double 的分辨率为 ~0.0000003 秒。

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

    6:40:24.6937,207,4070x006DF9EF

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

帮助解码 TDS 的资源