ADODB 无法以亚秒级精度存储 DATETIME 值
ADODB unable to store DATETIME value with sub-second precision
根据 ADODB 使用的数据类型 Microsoft documentation for the DATETIME column type, values of that type can store "accuracy rounded to increments of .000, .003, or .007 seconds." According to their documentation,ADODB 用于 DATETIME 列参数的 adDBTimeStamp(代码 135),"indicates a date/time stamp (yyyymmddhhmmss plus a fraction in billionths)." 然而,所有尝试(使用多个版本测试SQL 服务器,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])
此代码在上面指示的行上抛出异常,并显示错误消息 "Application uses a value of the wrong type for the current operation."这是 ADODB 中的已知错误吗?如果是这样,我还没有找到任何关于它的讨论。 (也许之前有过讨论,当 Microsoft 杀死 KB 页面时消失了。)如果值与文档匹配,怎么会是错误类型?
这是 SQL 服务器 OLEDB 驱动程序返回 more than 20 years 中的一个众所周知的错误;这意味着它永远不会被修复。
这也不是 ADO 中的错误。 ActiveX 数据对象 (ADO) API 是底层 OLEDB API 的薄包装。该错误存在于 Microsoft 的 SQL 服务器 OLEDB 驱动程序本身(所有驱动程序)中。他们永远不会,永远不会,永远不会现在修复它;因为 他们是鸡屎,不想维护现有代码,它可能会破坏现有应用程序。
所以bug一直发扬光大了几十年:
- SQOLEDB (1999) → SQLNCLI (2005) → SQL NCLI10 (2008) → SQLNCLI11 (2010) → MSOLEDB (2012)
唯一的解决办法不是将 datetime
参数化为 timestamp:
adTimestamp
(又名 DBTYPE_DBTIMESTAMP
、135
)
您需要将其参数化为 "ODBC 24 小时格式" yyyy-mm-dd hh:mm:ss.zzz 字符串:
adChar
(又名 DBTYPE_STR
、129
):2021-03-21 17:51:22.619
或者连同 ADO 特定类型字符串类型:
adVarChar
(200
): 2021-03-21 17:51:22.619
其他 DBTYPE_xxx 的呢?
您可能认为 adDate
(又名 DBTYPE_DATE
、7
)looks promising:
Indicates a date value (DBTYPE_DATE). A date is stored as a double, the whole part of which is the number of days since December 30, 1899, and the fractional part of which is the fraction of a day.
但不幸的是不是,因为它也将值参数化到服务器没有毫秒:
exec sp_executesql N'SELECT @P1 AS Sample',N'@P1 datetime','2021-03-21 06:40:24'
您也不能使用 adFileTime
,这看起来也很有希望:
Indicates a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (DBTYPE_FILETIME).
意味着它可以支持 0.0000001 秒的分辨率。
很遗憾,rules of VARIANT
s 不允许您将 FILETIME
存储在 VARIANT
中。由于 ADO 对所有值都使用变体,因此在遇到变体类型 64 (VT_FILETIME
) 时会抛出。
解码 TDS 以证实我们的怀疑
我们可以通过解码发送到服务器的数据包来确认 SQL 服务器 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 is represented in the following sequence:
- One 4-byte signed integer that represents the number of days since January 1, > 1900. Negative numbers are allowed to represent dates since January 1, 1753.
- One 4-byte unsigned integer that represents the number of one three-hundredths of a second (300 counts per second) elapsed since 12 AM that day.
这意味着我们可以将驱动程序提供的 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 Automation 使用 Double 来表示 datetime
.
Double 的分辨率约为 0.0000003 秒。
驱动程序有选项将时间编码到 1/300 秒:
6:40:24.693 → 7,207,407 → 0x006DF9EF
但它选择不这样做。错误:驱动程序。
有助于解码 TDS 的资源
根据 ADODB 使用的数据类型 Microsoft documentation for the DATETIME column type, values of that type can store "accuracy rounded to increments of .000, .003, or .007 seconds." According to their documentation,ADODB 用于 DATETIME 列参数的 adDBTimeStamp(代码 135),"indicates a date/time stamp (yyyymmddhhmmss plus a fraction in billionths)." 然而,所有尝试(使用多个版本测试SQL 服务器,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])
此代码在上面指示的行上抛出异常,并显示错误消息 "Application uses a value of the wrong type for the current operation."这是 ADODB 中的已知错误吗?如果是这样,我还没有找到任何关于它的讨论。 (也许之前有过讨论,当 Microsoft 杀死 KB 页面时消失了。)如果值与文档匹配,怎么会是错误类型?
这是 SQL 服务器 OLEDB 驱动程序返回 more than 20 years 中的一个众所周知的错误;这意味着它永远不会被修复。
这也不是 ADO 中的错误。 ActiveX 数据对象 (ADO) API 是底层 OLEDB API 的薄包装。该错误存在于 Microsoft 的 SQL 服务器 OLEDB 驱动程序本身(所有驱动程序)中。他们永远不会,永远不会,永远不会现在修复它;因为 他们是鸡屎,不想维护现有代码,它可能会破坏现有应用程序。
所以bug一直发扬光大了几十年:
- SQOLEDB (1999) → SQLNCLI (2005) → SQL NCLI10 (2008) → SQLNCLI11 (2010) → MSOLEDB (2012)
唯一的解决办法不是将 datetime
参数化为 timestamp:
adTimestamp
(又名DBTYPE_DBTIMESTAMP
、135
)
您需要将其参数化为 "ODBC 24 小时格式" yyyy-mm-dd hh:mm:ss.zzz 字符串:
adChar
(又名DBTYPE_STR
、129
):2021-03-21 17:51:22.619
或者连同 ADO 特定类型字符串类型:
adVarChar
(200
):2021-03-21 17:51:22.619
其他 DBTYPE_xxx 的呢?
您可能认为 adDate
(又名 DBTYPE_DATE
、7
)looks promising:
Indicates a date value (DBTYPE_DATE). A date is stored as a double, the whole part of which is the number of days since December 30, 1899, and the fractional part of which is the fraction of a day.
但不幸的是不是,因为它也将值参数化到服务器没有毫秒:
exec sp_executesql N'SELECT @P1 AS Sample',N'@P1 datetime','2021-03-21 06:40:24'
您也不能使用 adFileTime
,这看起来也很有希望:
Indicates a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (DBTYPE_FILETIME).
意味着它可以支持 0.0000001 秒的分辨率。
很遗憾,rules of VARIANT
s 不允许您将 FILETIME
存储在 VARIANT
中。由于 ADO 对所有值都使用变体,因此在遇到变体类型 64 (VT_FILETIME
) 时会抛出。
解码 TDS 以证实我们的怀疑
我们可以通过解码发送到服务器的数据包来确认 SQL 服务器 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 is represented in the following sequence:
- One 4-byte signed integer that represents the number of days since January 1, > 1900. Negative numbers are allowed to represent dates since January 1, 1753.
- One 4-byte unsigned integer that represents the number of one three-hundredths of a second (300 counts per second) elapsed since 12 AM that day.
这意味着我们可以将驱动程序提供的 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 Automation 使用 Double 来表示
datetime
.Double 的分辨率约为 0.0000003 秒。
驱动程序有选项将时间编码到 1/300 秒:
6:40:24.693 → 7,207,407 → 0x006DF9EF
但它选择不这样做。错误:驱动程序。