I have a linked table in SQL Server with a datetime column where I store a time-only value*. If I execute an UPDATE query without dbFailOnError it translates that command into a SELECT followed by individual UPDATE statements that execute one row at a time:
exec sp_executesql N'UPDATE "dbo"."Appeals" SET "HearingTime"=#P1
WHERE "AppealID" = #P2'
,N'#P1 datetime,#P2 int','1899-12-30 09:00:00',1
...
exec sp_executesql N'UPDATE "dbo"."Appeals" SET "HearingTime"=#P1
WHERE "AppealID" = #P2'
,N'#P1 datetime,#P2 int','1899-12-30 09:00:00',4
If I execute the exact same UPDATE query, but with dbFailOnError, I get this translation:
UPDATE "dbo"."Appeals" SET HearingTime={t '09:00:00'}
It is interesting that the dbFailOnError forces a more efficient UPDATE on the back end, but my real concern is with the time value itself.
In the first example, Access correctly sets the datetime to 12/30/1899 (MS Access's magic "zero" day). In the second case, that does not happen. The end result is that the first example "works" and the second one does not.
What I mean by that, is that if I view the HearingTime field in datasheet view, Access shows the first one as:
9:00:00 AM
And the second one shows as (as of this writing, 9/3/16 is today's date):
9/3/2016 9:00:00 AM
I have to assume this is a bug on Microsoft's part. Or am I missing something here? Do I have a better option than reporting the bug and just hoping that Microsoft fixes it someday?
*Yes I am aware there is a time datatype in SQL Server. It is not compatible with the MS Access datetime type, so it's of little use to me in an MS Access linked table.
I was able to reproduce your issue using queries with Access SQL Date/Time literals. Both
Dim cdb As DAO.Database
Set cdb = CurrentDb
cdb.Execute "UPDATE dbo_Appeals SET HearingTime=#10:00:00#", dbFailOnError
and
Dim cdb As DAO.Database
Set cdb = CurrentDb
cdb.Execute "UPDATE dbo_Appeals SET HearingTime=#1899-12-30 11:00:00#", dbFailOnError
resulted in [HearingTime] values with the current date (2016-09-03), not the "zero" date.
However, passing the full date/time as a string seems to work
Dim cdb As DAO.Database
Set cdb = CurrentDb
cdb.Execute "UPDATE dbo_Appeals SET HearingTime='1899-12-30 12:00:00'", dbFailOnError
(Omitting the date part in the string value results in [HearingTime] values with the SQL Server DATETIME "zero" date of 1900-01-01.)
And, even better, a parameterized query with a DAO.QueryDef object also appears to work correctly
Dim cdb As DAO.Database
Set cdb = CurrentDb
Dim qdf As DAO.QueryDef
Set qdf = cdb.CreateQueryDef("", _
"PARAMETERS prmHearingTime DateTime;" & _
"UPDATE dbo_Appeals SET HearingTime=[prmHearingTime]")
qdf!prmHearingTime = #10:00:00 AM#
qdf.Execute dbFailOnError
That's a good question, and I can reproduce it.
It seems to be a bug in the ODBC driver, because the behaviour you demonstrate is that of SQL Server which the ODBC driver should compensate for, as it does in your first example by providing the full date-time string that will translate correctly when read back in Access.
I can see no way to work around it other than, when you read back the data, to apply TimeValue to the date-time values. This, however, will cease any use of an index of your time field.
And data type Time of SQL Server cannot be used for many things as the ODBC driver reads these as text.
Related
I have an MS Access program that constructs a SQL INSERT statement that inserts rows from an Access table into a linked SQL Server table with the same table structure. The SQL Server table may or may not have an IDENTITY primary key.
The MS Access source table has its corresponding primary key pre-populated, and these values must be inserted into the SQL Server table for referential reasons. SQL Server won't allow this unless I toggle the IDENTITY_INSERT property, but to decide if I need to toggle I need to determine if there is an IDENTITY column in the first place.
So in VBA from MS Access I need to determine if the linked SQL Server table has an IDENTITY column.
This question is covered here but for Transact-SQL.
I actually can get what I need using a pass-thru query, so it is feasible.
Create a pass-thru query to the SQL Server database.
In the SQL view enter
SELECT OBJECTPROPERTY(OBJECT_ID(N'table_name'), 'TableHasIdentity') AS 'HasIdentity'
and execute, it returns a 1-row datasheet with column HasIdentity that has value 1 if the table has an IDENTITY column.
The problem is that in VBA I cannot get at this datasheet. Given the query runs OK from MS Access I think the datasheet must correspond to a recordset and the following code should work.
Public Function metaODBCExecuteSQL(ByVal pstrConn As String) As Recordset
Dim qdf As QueryDef
Dim rst As Recordset
Set qdf = CurrentDb.CreateQueryDef("ODBC Execute")
qdf.ReturnsRecords = True
qdf.Connect = pstrConn
qdf.SQL = "SELECT OBJECTPROPERTY( OBJECT_ID(N'table_name'), 'TableHasIdentity') AS 'HasIdentity'"
Set rst = qdf.OpenRecordset(, dbSeeChanges)
qdf.Close
Set metaODBCExecuteSQL = rst
END Function
Instead the set rst line returns a run-time error 3219 invalid operation (with or without dbSeeChanges, which I believe is necessary for working with tables with IDENTITY).
Can anyone point out how I can retrieve the result set from the SELECT OBJECTPROPERTY query? What is wrong with my code, or is there another way.
After all if the MS Access UI can do it there must be a way.
If I create and save the query, this works great here:
? metaODBCExecuteSQL("")(0)
' Returns 0 or 1.
using this function and adjusting table_name to some valid table name:
Public Function metaODBCExecuteSQL(ByVal pstrConn As String) As Recordset
Dim qdf As DAO.QueryDef
Dim rst As DAO.Recordset
Set qdf = CurrentDb.QueryDefs("ODBC Execute")
qdf.SQL = "SELECT OBJECTPROPERTY( OBJECT_ID(N'table_name'), 'TableHasIdentity') AS 'HasIdentity'"
Set rst = qdf.OpenRecordset()
qdf.Close
Set metaODBCExecuteSQL = rst
End Function
I'm operating on a virtual machine where my SQL server and MS Access sit. I set up an ODBC connection from Access to SQL and linked a few tables. I can perform the usual operations on these tables (select/update etc.).
BUT, I'm unable to run a stored procedure for some weird reason! The procedure runs perfectly on SSMS but not when I call it from Access VBA. Following is the code I'm using to execute the proc (I need to pass 3 parameters as well, but I've excluded that from the code below for simplicity):
With CurrentDb.QueryDefs("qPass")
.SQL = "exec [HS].[spGetXMLExtract]"
.Execute
End With
The error returned by MS Access is
Invalid SQL statement: expected 'DELETE', 'INSERT', 'PROCEDURE', 'SELECT', or 'UPDATE'.
Don't know if this is happening because:
1. I've got these applications on a VM; and/or
2. I need to somehow link the stored procedure to MS Access just like I did for the tables (perhaps this can't be done).
I guess there are convoluted methods I 'might' be able to adopt but I don't want to. Example:
1. Create a table in SQL with columns that store the parameters; Write an AFTER UPDATE Trigger on that table which executes my stored procedure; Fire an 'Update' query from MS Access that would update the parameters in that table and a SQL trigger then gets fired. OR;
2. Eliminate the stored procedure from the equation completely and execute it's individual statements (select/update/insert etc.) through a Sub in MS Access. Don't know if this would cause problems with creating temp tables though.
Can someone please advise on this, this is a real blocker!
Thanks in advance!
You will receive that error message if the QueryDef does not have a valid "ODBC;..." connection string as its .Connect property. That is how Access identifies the QueryDef as a pass-through query.
If you already have an ODBC linked table defined, you can use its .Connect property value for the .Connect property of the QueryDef, like so:
Dim cdb As DAO.Database
Set cdb = CurrentDb
Dim qdf As DAO.QueryDef
Set qdf = cdb.CreateQueryDef("")
qdf.Connect = cdb.TableDefs("dbo_table1").Connect ' grab .Connect string from linked table
qdf.sql = "exec [HS].[spGetXMLExtract]"
qdf.ReturnsRecords = False
qdf.Execute
... or, if the stored procedure does in fact return a result set:
Dim cdb As DAO.Database
Set cdb = CurrentDb
Dim qdf As DAO.QueryDef
Set qdf = cdb.CreateQueryDef("")
qdf.Connect = cdb.TableDefs("dbo_table1").Connect ' grab .Connect string from linked table
qdf.sql = "exec [HS].[spGetXMLExtract]"
qdf.ReturnsRecords = True
Dim rst As DAO.Recordset
Set rst = qdf.OpenRecordset(dbOpenSnapshot)
Do Until rst.EOF
' do stuff
rst.MoveNext
Loop
rst.Close
I do not have access right now to MS-Access, but it seems to me that the good syntax is:
`strSQL = "exec [HS].[spGetXMLExtract]"
With CurrentDb.QueryDefs(strSQL)
'Fill parameters
.Parameters(0) = My first param
.Parameters(1) = My 2nd param
.Parameters(2) = My last param
'Execute the query
.Execute
End With `
I hope that will help !
I know this will be a common problem, nevertheless I didn't find any solution.
I migrated my access database into SQL Server Express. SELECT and UPDATE work well, actually better that with the Access engine. But I can't get standard insertions running...
This does not work:
Set rc = CurrentDb.openRecordset("SELECT * FROM DBLog WHERE false")
rc.AddNew
rc.update '-->ERROR
rc.close
This does not work either:
DoCmd.RunSQL("INSERT INTO [test2].dbo.DBLog (type) VALUES (6)")
This does work:
sending the above SQL with pass through. So its not the SQL Servers problem.
I have set IDENTITY and PRIMARY in the Database. Access also knows the primary. In design view although there is no "Autonumber", and this may be the problem. But how to resolve that?
As a general rule, after migrating to SQL server for the back end, then a typical and common open reocrdset of
Set rst = CurrentDb.OpenRecordset("Contacts")
Needs to become
Set rst = CurrentDb.OpenRecordset("Contacts", dbOpenDynaset, dbSeeChanges)
So, in your case:
Set rc = CurrentDb.openRecordset("SELECT * FROM DBLog WHERE false" dbOpenDynaset, dbSeeChanges)
Since you have a linked table named DBLog, I would execute an INSERT statement, similar to the one which worked in SQL Server itself, against that linked table.
Dim db As DAO.Database
Dim strInsert As String
strInsert = "INSERT INTO DBLog ([type]) VALUES (6)"
Set db = CurrentDb
db.Execute strInsert, dbFailonerror
I enclosed the field name type in square brackets because it is a reserved word.
I have just learned about the pass through queries in MS-ACCESS.
I have a SQL SERVER backend and
if I'm right, for a query access loads all records before to do the where clause... so what would be the point of having a SQL SERVER backend?
That's why I want to try using pass through queries as much as possible but is there a way I can get the connection string from my linked tables for my pass through queries?
I tried CurrentDb.TableDefs("One of my table name").Connect in the ODBC Connect Str property but I got the error saying it's an invalid connection string.
It would be nice because I know I will have to change the connection soon so I wouldn't have to edit the connection string at many places.
Thank you.
I'm not sure what you meant here: "for a query access loads all records before to do the where clause"
If the WHERE clause can be applied at the server, ODBC will translate it to the server's language, and only the matching rows will be sent back to Access:
WHERE date_field >= #2011-01-01# AND date_field < #2012-01-01#
That WHERE clause would limit the rows sent to Access to only those whose date_field values are from 2011.
However, if a WHERE clause includes functions which must be evaluated by Access, ODBC must retrieve all candidate rows and hand them over to the Access db engine so it can perform the evaluation.
WHERE Format(date_field, 'yyyy') = '2011'
But for your actual question ... connection string for pass through queries ... consider the following code example. I have an ODBC link named dbo_foo whose source table in SQL Server is [dbo].[foo]. So I can grab the .Connect property from dbo_foo and use it for the .Connect property of a pass through query based on the same server table.
Public Sub CreatePassThruQuery()
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim strConnect As String
Set db = CurrentDb
strConnect = db.TableDefs("dbo_foo").Connect
Set qdf = db.CreateQueryDef("qryDbo_Foo")
qdf.Connect = strConnect
qdf.SQL = "SELECT * FROM [dbo].[foo];"
qdf.Close
Set qdf = Nothing
Set db = Nothing
End Sub
Still when you change the .Connect property of the table, you will also need to do it for the query. If you have many of them and/or change the connections frequently, it may be worth the effort to create a VBA procedure to update them. Alternatively, you might use a DSN for the .Connect property of the table and matching query. Then revise the DSN as needed. A pitfall with that approach is that, if other people will be using your application, you would need to manage the DSNs on multiple machines.
I have the following UDF in excel which uses ADO to connect to my MSSQL server. There it should execute the scalar udf "D100601RVDATABearingAllow".
For some reason the parameters that I try to append are not send to the sql server. At the server only:
SELECT dbo.D100601RVDATABearingAllow
arrives.
MY EXCEL UDF:
Function RVDATA(Fastener) As Long
Dim cnt As ADODB.Connection
Dim rst As ADODB.Recordset
Dim Cmd1 As ADODB.Command
Dim stSQL As String
Const stADO As String = "Provider=SQLOLEDB.1;Data ................"
'----------------------------------------------------------
Set cnt = New ADODB.Connection
With cnt
.ConnectionTimeout = 3
.CursorLocation = adUseClient
.Open stADO
.CommandTimeout = 3
End With
'----------------------------------------------------------
Set Cmd1 = New ADODB.Command
Cmd1.ActiveConnection = cnt
Cmd1.CommandText = "dbo.D100601RVDATABearingAllow"
Cmd1.CommandType = adCmdStoredProc
'----------------------------------------------------------
Set Param1 = Cmd1.CreateParameter("Fastener", adInteger, adParamInput, 5)
Param1.Value = Fastener
Cmd1.Parameters.Append Param1
Set Param1 = Nothing
'----------------------------------------------------------
Set rst = Cmd1.Execute()
RVDATA = rst.Fields(0).Value
'----------------------------------------------------------
rst.Close
cnt.Close
Set rst = Nothing
Set cnt = Nothing
'----------------------------------------------------------
End Function
When I use adCmdStoredProc the whole thing fails and in the vba debugger the properties of the recordset has a lot of "Operation is not allowed when object is closed" (may sound a bit different, the message is translated)
When I don't use adCmdStoredProc I get the message that the variable Fastener was not provided.
I think that maybe something is wrong in the way I open the recordset.
In other treads I read about using the "SET NOCOUNT ON" option, but that did not work either.
Does anyone have a idea?
Regards Lumpi
Ran into this error as well (in my case I am using a Stored Procedure to retrieve some information). I had made some changes which caused the execution to malfunction.
The error disappeared when I put SET NOCOUNT ON as the first statement of the Stored Procedure.
You do not need to SELECT the server side function, just provide its name ("[tra-CAE400-1].dbo.D100601RVDATABearingAllow") in the .CommandText property.
Also you should set the .CommandType property to "stored-procedure" (property reference on w3schools.com).
Then adodb will know that you are talking about calling a function, and not trying to send a plain sql-command.
Chances are that it will then allow you to define the parameters on the command object.
But the parameters you define on the command object should correspond exactly (in name and type) to the ones that are defined as the arguments of the function in the sql server.
An example from microsoft.com on using the command-object with a stored procedure
ADO Reference on microsoft.com
Another possible cause of this is debug statements. I just spent far too long trying to work out why this wouldn't work for me, the Proc on the database worked fine, the data it was supposed to insert was inserted, the VBA code worked fine, but there was nothing in the recordset.
Final solution was to go through the procs that had been built and remove the PRINT statements.
To test if this is the problem, run your proc on SQL Server manually, then look at the messages tab of the results, if there's anything there other than "Command(s) completed successfully." you need to eliminate those messages. "SET NOCOUNT ON" will get rid of the row count messages, but there may be others.
I'm assuming that after 5 years the OP has solved this particular problem, so this is just for anyone like me that finds this while searching for the same problem.
I also ran into this with a stored procedure. Did you SET NOCOUNT = OFF; at the bottom of your code? That is what worked for me after lots of googling. Also, if you have any other code that runs, you have to wrap it in Nocount = on/off, INCLUDING insert and update statements. You would think that an insert statement wouldn't matter but wrapping the code that way is what kept me from committing suicide today.
In our shop we often use lines like this in our stored procedures to assist with debugging:
RAISERROR('Debug message here',0,1) WITH NOWAIT;
This also breaks opening a recordset in Excel vba. I believe the complete answer for this question is, in the stored procedure:
use SET ROWCOUNT OFF
remove all PRINT statements
remove all RAISEERROR statements used for debugging (ie severity of 0)