For Each If Statement - Skip NULLs - sql-server

I have created a macro/some VBA to UPDATE a SQL Server table which works fine.
In short the code pulls a defined amount of records from the the table to excel and then the end user updates some specific information and then clicks update. A connection is created to the table and an SQL update statement runs which updates the relevant records.
The problem is where the user has not had to update a NULL field (NULL is in the SQL Server table but shows as 'empty' in Excel), when the use clicks update the SQL statement is forcing the NULL to an 'empty' entry.
To get round this I would like my code in the For Each statement to check if the cell/record is NULL or Empty and to skip to the NEXT row so the SQL Execute command is not carried out.
Here is the VBA in question:
cnn.Open cnnstr
Dim row As Range
For Each row In [tbl_data].Rows
uSQL = "UPDATE BREACH_DATA SET [VAL_BREACH_REASON] = '" & (row.Columns(row.ListObject.ListColumns("VAL_BREACH_REASON").Index).Value) _
& "' ,[VAL_BREACH_DETAIL] = '" & (row.Columns(row.ListObject.ListColumns("VAL_BREACH_DETAIL").Index).Value) _
& "' ,[VAL_VALID] = '" & (row.Columns(row.ListObject.ListColumns("VAL_VALID").Index).Value) _
& "' ,[VAL_NOTES] = '" & (row.Columns(row.ListObject.ListColumns("VAL_NOTES").Index).Value) _
& "' WHERE [ATD_NUMBER] = '" & (row.Columns(row.ListObject.ListColumns("ATD_NUMBER").Index).Value) & "'"
'Debug.Print uSQL
cnn.Execute uSQL
Next
cnn.Close
Set cnn = Nothing
Any suggestions
Kind Regards
Dino

You are updating SQL Server data directly with strings from a cell. This is a classic example of opening a door for injection attacks - users can do all kinds of bad, bad things to your database. But given that you fix that here is a way to check that each cell is not empty or null (I assume if one of the fields are not empty or null you want to update...):
if not
(
(isempty(row.Columns(row.ListObject.ListColumns("VAL_BREACH_REASON").Index).Value)
and isnull(row.Columns(row.ListObject.ListColumns("VAL_BREACH_REASON").Index).Value)
and do same for the other cell values....
)
then update....

Related

MS Access and Error 3035 "System resources exceeded."

I see there are a lot of questions on this issue, but I thought I'd add to it again. I'm trying to run Pass Through queries to put the load on the server instead of wimpy Access. Almost every table I have is stored in a SQL server, but I have a large table I have to loop through and it's much faster if I copy it to a local table and then loop through it. Otherwise, everything is faster or fast enough when going through the pass through functions.
The table in question currently holds about 25k lines and if I just write a query to have access copy the local table to SQL server it takes about 1 hour. However, if I use a pass through query with insert, I can copy it over in about 47 seconds. My problem seems to come when I try to pass too long of a string of text.
Here is my function that run the pass through query
Public Sub RunPassThruQdf(sqlCode As String, Optional isTestDB As Boolean = False)
Dim qdf As QueryDef
Set qdf = CurrentDb.QueryDefs("vbaSQL")
If isTestDB Then
qdf.Connect = "ODBC;DSN=DataWarehouse_Test;Description=DataWarehouse_Test;UID=**username**;PWD=**password**;APP=Microsoft Office;DATABASE=DataWarehouse_test"
'The above line is where the error happens when I debug
Else
qdf.Connect = "ODBC;DSN=DataWarehouse;Description=DataWarehouse;UID=**username**;PWD=**password**;APP=Microsoft Office;DATABASE=DataWarehouse"
End If
qdf.ReturnsRecords = False
qdf.sql = sqlCode
Do Until InStr(qdf.sql, " ") = 0
qdf.sql = Replace(qdf.sql, " ", " ")
Loop
qdf.Execute
qdf.Close
End Sub
You'll notice that it connects with a special login (redacted) because I can't make changes to the server myself, but that user account can. And that last loop removes double spaces until everything is separated by a single space to reduce the string size as much as possible.
And this is the function that copies the local table to the server table
Public Sub LoadUnidentifiedFromLocal()
Dim rst As New RecordsetClass: rst.OpenR "tblUnidentifiedParts_Local"
Dim dtm As Date: dtm = Now
Dim baseSQL As String: baseSQL = "INSERT INTO [DataWarehouse_test].[dbo].[mT_SalesAttributeDB_UnidentifiedParts] (ID, PartNumber, FamilyNumber, AutoNote, ManualNote, Created, Updated) VALUES "
Dim sql As String
RunPassThruQdf "SET IDENTITY_INSERT [DataWarehouse_test].[dbo].[mT_SalesAttributeDB_UnidentifiedParts] ON", True
Dim i As Integer: i = 1
Do Until rst.EOF
If sql = vbNullString Then sql = baseSQL
If i Mod 500 = 0 Then
RunPassThruQdf sql, True
'Debug.Print i & ": " & Format(Now - dtm, "hh:mm:ss") & " (" & Format(Len(sql), "#,##0") & ")"
'DoEvents
sql = baseSQL
End If
Dim addSQL As String: addSQL = "(" & rst.Fields("ID") & ", '" & rst.Fields("PartNumber") & "', '" & rst.Fields("FamilyNumber") & "', '" & rst.Fields("AutoNote") & "', '" & rst.Fields("ManualNote") & "', '" & rst.Fields("Created") & "', '" & rst.Fields("Updated") & "')"
If sql = baseSQL Then
sql = sql & addSQL
Else
sql = sql & ", " & addSQL
End If
rst.MoveNext
i = i + 1
Loop
If sql <> vbNullString Then
RunPassThruQdf sql, True
Debug.Print i & " " & Format(Now - dtm, "hh:mm:ss")
End If
RunPassThruQdf "SET IDENTITY_INSERT [DataWarehouse_test].[dbo].[mT_SalesAttributeDB_UnidentifiedParts] OFF", True
rst.CloseR
End Sub
Where I have that mod 500 is me trying to figure out how many lines I can copy over until I get that System resources exceeded error. You'll probably also notice I have a special rst class I made. But just know that it mimics the normal one, I just have some special functions in there so it was my version of inheritance since VBA doesn't support that.
The first time I got it I googled and found that someone was able to fix their error by change the max lock to 1 million. And that worked for me too, until I exceeded it. DAO.DBEngine.SetOption dbMaxLocksPerFile, 1000000
I know it's not a problem with my computer, it's 6 core Xeon W-10855M with 64GB of ram. But I will see that error even after restart for a while and then it will just stop and work again until I try to send it too much and then I'm stuck seeing it for a bit. What is weird is even restarting my computer will not fix the issue. And I've tried compress and repair and that won't fix it either. So I'm not clear what changes so that it stops reporting that.
But my first question is if increasing the max locks per file helped, is there a way to clear what locks are currently there? My second question would be how big of a string can I send with a pass through, I think I saw somewhere in the neighborhood of 65k before I got the message (That was when I had it set at i mod 500.

Quote newly created id in message box after SQL Insert in MS Access

I have a VBA script in MS Access that inserts a new record into a SQL Server table based on some inputs on the form.
Private Sub Allocate_Click()
Dim strSQL As String
strSQL = "INSERT INTO dbo_ALLOCATIONS " _
& "([Employee No],[Device Serial Number],[Start Date]) VALUES " _
& "('" & Me.EmployeeNumber & "', '" & Me.SerialNumber & "', '" & Me.StartDate & "')"
CurrentDb.Execute strSQL, dbFailOnError
Dim ReceiptNo As dao.Recordset
Set ReceiptNo = CurrentDb.OpenRecordset("select ##identity")
MsgBox "Device Successfully Allocated. Receipt ID is " & CurrentDb.OpenRecordset("select ##identity")
End Sub
At the end, I query the ID that was auto-incremented during the insert. I want to then quote this in a Message Box so the user can make use of it elsewhere. How can I get it to quote like that in the MsgBox command? The one I have at the moment causes lots of issues around the fact I can't combine this command, and when using only 'ReceiptNo' it says it's not a string.
There should only ever be a single result in the recordset. Try changing your last line to:
MsgBox "Device Successfully Allocated. Receipt ID is " & ReceiptNo(0)
It is not possible to print an entire recordset. You need to refer to the column that you want to print and loop through the recordset while the end of file is not reached. Then output your column as string. I did not test it though.
While not ReceiptNo.EOF
Msgbox Str(ReceiptNo!Identity)
ReceiptNo.moveNext
Wend

In MS Access SQL Server Linked Table pass through query

I have a query against a linked table in MS Access that uses the getdate() function of SQL Server. However, I get this error when I attempt to run the query:
Undefined function GetDate in function
How do I create a linked table that allows the use of SQL Server T-SQL syntax? I see that this is called a pass through query but I don't know how to set it up to use the connection on the linked table as a pass through query.
Currently using Access 2010. The query is:
select getdate()
If it helps, I used the following vba code that generates the table link to SQL Server:
Function LinkTable(LinkedTableAlias As String, Server As String, Database As String, SourceTableName As String, OverwriteIfExists As Boolean, Username As String, Password As String)
'This method will also update the link if the underlying table definition has been modified.
If (InStr(1, LinkedTableAlias, "MSys") > 0) Then
Log "Skipping " & LinkedTableAlias
Exit Function
End If
'The overwrite parameter will cause it to re-map/refresh the link for LinktedTable Alias, but only if it was already a linked table.
' it will not overwrite an existing query or local table with the name specified in LinkedTableAlias.
'Links to a SQL Server table without the need to set up a DSN in the ODBC Console.
Dim tdfLinked As DAO.TableDef
' Open a database to which a linked table can be appended.
Dim dbsCurrent As Database
Set dbsCurrent = CurrentDb()
'Check for and deal with the scenario ofthe table alias already existing
If TableNameInUse(LinkedTableAlias) Then
'If InStr(dbsCurrent.TableDefs(LinkedTableAlias).Connect, "AccessBackup") Then
' Exit Function
'End If
If (Not OverwriteIfExists) Then
Log "Can't use name '" + LinkedTableAlias + "' because it would overwrite existing table."
Exit Function
End If
'delete existing table, but only if it is a linked table
'If IsLinkedTable(LinkedTableAlias) Then
dbsCurrent.TableDefs.Delete LinkedTableAlias
dbsCurrent.TableDefs.Refresh
'Else
' Log "Can't use name '" + LinkedTableAlias + "' because it would overwrite an existing query or local table."
' Exit Function
'End If
End If
'Create a linked table
Set tdfLinked = dbsCurrent.CreateTableDef(LinkedTableAlias)
tdfLinked.SourceTableName = SourceTableName
tdfLinked.Connect = "ODBC;DRIVER={SQL Server};SERVER=" & Server & ";DATABASE=" & Database & ";UID=" & Username & ";PWD=" & Password & ";"
On Error Resume Next
dbsCurrent.TableDefs.Append tdfLinked
If (err.Number = 3626) Then 'too many indexes on source table for Access
err.Clear
On Error GoTo 0
If LinkTable(LinkedTableAlias, Server, Database, "vw" & SourceTableName, OverwriteIfExists, Username, Password) Then
Log "Can't link directly to table '" + SourceTableName + "' because it contains too many indexes for Access to handle. Linked to view '" & "vw" & SourceTableName & "' instead."
LinkTable = True
Else
Log "Can't link table '" + SourceTableName + "' because it contains too many indexes for Access to handle. Create a view named '" & "vw" & SourceTableName & "' that selects all rows/columns from '" & SourceTableName & "' and try again to circumvent this."
LinkTable = False
End If
Exit Function
End If
On Error GoTo 0
'** Turn on error handling
On Error GoTo ErrorHandler:
tdfLinked.RefreshLink
LinkTable = True
Exit Function
ErrorHandler:
Log "refreshlink failed for " & tdfLinked.Name
LinkTable = True
I don't quite understand this statement:
How to I create a linked table that allows the use of SQL Server T-SQL
syntax?
But this is how you convert an existing MS Access querydef to a pass through query:
Go to design mode in the query, press the Query menu command, then SQL Specific then Pass Through
See this for screenshots.
http://www.mssqltips.com/sqlservertip/1482/microsoft-access-pass-through-queries-to-sql-server/
The reason why you are getting the error is that GETDATE() is not a function inside MSAccess. You probably need Now() to get the date and time or you may use Date() which provides the date
Here's a quick and dirty VBA way to create a pass-through query:
Set qdf = CurrentDb.CreateQueryDef("testqry")
' this is just your connection string
qdf.Connect = "ODBC;Driver={SQL Server};Server=MSSQL1; Database=MyDB;Trusted_Connection=Yes"
'anything here gets passed directly to and executed on the SQL Server
qdf.SQL = "select getdate()"
Set qdf = Nothing
Now you can use "testqry" as if it's any other Access query (as far as SELECTing from it goes, anyway)
Simple save your t-sql query as a pass-though
Select GetDate()
Then in VBA code, you can go:
TheSqlDate = currentdb.QueryDefs("qPass").OpenRecordset()(0)
Using ADO, and hardcoding connection strings, and the HUGE whacks of other code posted here is just a way to rack up billable hours and create world poveity. My posted solution IS ONLY ONE LINE OF CODE!

Changing DROP TABLE code to APPEND/INSERT

This code currently is using a DROP TABLE to transfer data from Access to SQL Server, that is when it was intended for one person to use. Now it is going to be used by multiple people so I need to alter the code to APPEND. I am trying to figure out how to get it to check the destination table to see if the record already exists, if it does it will skip over it, if not it will write that employees data to the table. The SQL table is prebuilt and it has a ROW ID column and TIME STAMP column that the access table does not have in order to keep track of the records being entered.
The code currently looks like:
Public Function Update()
Dim cdb As DAO.Database, qdf As DAO.QueryDef
Dim err As DAO.Error
Const DestinationTableName = "AC_CDData"
Const ConnectionString = _
"ODBC;" & _
"Driver={SQL Server Native Client 10.0};" & _
"Server=SERVER;" & _
"Database=DB;" & _
"UID=ID;" & _
"PWD=PW;"
Set cdb = CurrentDb
Set qdf = cdb.CreateQueryDef("")
qdf.Connect = ConnectionString
qdf.SQL = _
"IF EXISTS " & _
"(" & _
"SELECT * FROM INFORMATION_SCHEMA.TABLES " & _
"WHERE TABLE_NAME='" & DestinationTableName & " '" & _
") " & _
"DROP TABLE [" & DestinationTableName & "]"
qdf.ReturnsRecords = False
On Error GoTo Update_qdfError
qdf.Execute dbFailOnError
On Error GoTo 0
Set qdf = Nothing
Set cdb = Nothing
DoCmd.TransferDatabase _
acExport, _
"ODBC Database", _
ConnectionString, _
acTable, _
"CDData", _
DestinationTableName, _
False
Exit Function
Update_qdfError:
For Each err In DAO.Errors
MsgBox err.Description, vbCritical, "Error " & err.Number
Next
End Function
The DoCmd.TransferDatabase does a complete transfer of the data from the Access table, to the SQL Server database. This function can not do partial inserts based on existing records.
What you can do, however, is export the data into a temporary new table (without dropping the existing table), followed by executing an SQL MERGE statement, to incorporate the new records of the temporary table, with the existing table.
You'll need to know how to perform SQL queries from VBA against your SQL database, and you'll need to know how to use the SQL MERGE statement. A google search will quickly give you the answer to both.
Just setup a simple standared linked table to SQL server in Access.
Since the column of the existing record is a primary key (or simply an index set = unique), then you only need ONE LINE of code to append your data.
This will work:
CurrentDb.Execute "INSERT INTO AC_CDData SELECT * FROM CDData;"
Any row that already exists will be ignored due to key (or index) violation. Thus only new non existing records will be appended with the above one line of code.
Edit:
As for the target table being a SQL linked table? Your application on startup does not care if you have some linked tables. Your application does not use nor care about such linked tables unless you use them.
The assuming here is you have a local data table. The linked table ONLY comes into play when you going to do the export. As noted you cannot export unless you are connected to SQL server. The “code” or testing if you are connected is not dealt with in this question but even if it was part of the question in both cases you still have to test for a connection and once again when you do finally determine you have a connect and you decide to export then again the linked table as per above will work.
So there is really no reason I can think of as to why having a linked table that is ONLY used during the append to the SQL table will be any kind of issue or problem.
As stated, then the one line of VBA code should suffice here.
In case of concurrent usage the process need to be changed:
DestinationTableName must be unique per session (thread) and need to be changed (from constant to variable)
Tabel with DestinationTableName neame need and need to be dropped before the export data (as it was in current solution).
DoCmd.TransferDatabase will not change - the data will be exported into the unique temp table.
For simplifying the Merge process the stored procedure can be written on the SQL Server side (with all needed to APSERT logic) with the source table name as parameter. This SP need to be called after the DoCmd.TransferDatabase completion
In this case the flow will be the following:
DestinationTableName unique name generation
Dropping this table on the SQL Server side (qdf.Execute ...)
Export data to the temp table (DoCmd.TransferDatabase ...)
Merge data in the SQL Server side (call to created SP with needed logic of update / insert)

ASP Classic and SQL Server 2008 giving strange response

Started getting this error it seems since we upgraded to SQL Server 2008.
When inserting into the db and then returning the identity i get a 'Item cannot be found in the collection corresponding to the requested name or ordinal' error.
Here is the code:
SQL = "INSERT INTO PageFeatures(nPageFeatureFlagId,nPageFeatureFeatureId,nPageFeaturePageId) VALUES(" & nTemplateFlagId & "," & nFeatureId & "," & nPageId & "); SELECT SCOPE_IDENTITY() As nPageFeatureId;"
objrs.open SQL,objConn,1,1
nPageFeatureId = objrs("nPageFeatureId")
objrs.close
The insert is working as the record is in the db. It's not returning the id for some reason. It works fine and returns the id when running in SSMS. But ASP can't see the returned id so some reason.
Any help would be greatly appreciated!
You may have to try moving the recordset on? e.g.
SQL = "INSERT INTO PageFeatures(nPageFeatureFlagId,nPageFeatureFeatureId,nPageFeaturePageId) VALUES(" & nTemplateFlagId & "," & nFeatureId & "," & nPageId & "); SELECT SCOPE_IDENTITY() As nPageFeatureId;"
objrs.open SQL,objConn,1,1
objrs.NextRecordset
nPageFeatureId = objrs("nPageFeatureId")
objrs.close
Raj's comment about SQL injection is still relevant. :)
EDIT: Elaboration is that there are two recordsets in play here. The first is an empty one created by the insert statement, the second one is caused by everything after the semi-colon ;. This is the field you want. So initially, your objrs is attached to the first of its collection of recordsets (not rows in a recordset), you move to the NextRecordset - and you can then deal with that how you please.
Sorted:
SQL = "INSERT INTO PageFeatures(nPageFeatureFlagId,nPageFeatureFeatureId,nPageFeaturePageId) VALUES(" & nTemplateFlagId & "," & nFeatureId & "," & nPageId & ");"
objConn.execute(SQL)
Set oReturnValueRS = objConn.Execute("SELECT SCOPE_IDENTITY()")
nPageFeatureId = oReturnValueRS(0).Value
oReturnValueRS.close : set oReturnValueRS = nothing

Resources