VBA Wait for DoCmd.RunSQL OR Better Query - Ms Access - sql

I have a query which uses an array of tables (stored as a string array) to loop through my tables and perform a Delete/Update set of queries. The queries are identical other than the table names, hence using a loop to iterate through using the table name as a variable.
The problem is, my Delete query locks the table and the Update query runs too quickly afterward; I get a "db is locked" error.
I need either of two things:
A way to tell VBA to "wait for previous command" OR
A way to concatenate these queries into one (or two) queries: one to delete the database rows and another to import the new ones. With this I could just run the queries from a standard access query (which should allocate proper time, finish queries etc)
The only catch to this is that there are parent-child relations, so the parent table has to be updated before its children (currently accomplished through array ordering).
Here's the current code which (sometimes) produces the "DB locked/in use" message:
For i = 0 To UBound(tables)
'Delete all data first
sql = "DELETE * FROM " & tables(i)
DoCmd.RunSQL sql
'Update all data second
sql = "INSERT INTO " & tables(i) & " IN """ & toDB & """ SELECT " & tables(i) & " .* FROM " & tables(i) & " IN """ & fromDB & """;"
DoCmd.RunSQL sql
Next
Should clarify: the queries take one backend's (fromDB) rows from identical tables and pushes it to another backend's (toDB) rows
EDIT: In response to the questions regarding INSERT INTO, my problem with that is if I add fields to the toDB, it will delete them if I overwrite. The reason I have to do this backdoor approach is because the database is still in development, but is also being used with select tables. Updates and feature improvements are done daily. I cannot use a simple split-backend either, because the other computer accessing the database is not always on the network (we have to manually sync it when it returns to the network), so I work on one backend and it works on another, identical(ish, minus my schema update) backend.

You can use ADO instead of DoCmd.RunSQL to execute your SQL synchronously.
Dim cmd As ADODB.Command
Dim cnn As New ADODB.Connection
Set cnn = CurrentProject.Connection
For i = 0 To UBound(tables)
Set cmd = New ADODB.Command
With cmd
.CommandType = adCmdText
.ActiveConnection = cnn
.CommandText = "DELETE * FROM " & tables(i)
.Execute
End With
Set cmd = New ADODB.Command
With cmd
.CommandType = adCmdText
.ActiveConnection = cnn
.CommandText = "INSERT INTO " & tables(i) & " IN """ & toDB & """ SELECT " & tables(i) & " .* FROM " & tables(i) & " IN """ & fromDB & """;"
.Execute
End With
Next
You could also add cnn.BeginTrans and cnn.CommitTrans to make the two statements Atomic.

Try something like this (Note I've commented your DoCmd.RunSQL. I changed it with db.Execute:
Sub DeleteInsertData(tables() As String, toDB As String, fromDB As String)
Dim db As DAO.Database
Dim i As Integer
Dim SQL As String
Set db = CurrentDb()
For i = 0 To UBound(tables)
'Delete all data first
SQL = "DELETE * FROM " & tables(i)
db.Execute SQL, dbFailOnError
'DoCmd.RunSQL SQL
'Update all data second
SQL = "INSERT INTO " & tables(i) & " IN """ & toDB & """ SELECT " & tables(i) & " .* FROM " & tables(i) & " IN """ & fromDB & """;"
db.Execute SQL, dbFailOnError
'DoCmd.RunSQL SQL
Next
Set db = Nothing
End Sub

I'm no expert, but I don't think you need the DELETE part. SELECT INTO is the syntax for making a table. If the table already exists, it's overwritten.

If I need to wait for something, I tend to use DoEvents to allow windows to process any other actions that have been put on hold.
From the help: "Yields execution so that the operating system can process other events."
Found this:
.BeginTrans then
.CommitTrans dbForceOSFlush
as a way to force updates to be written before continuing

Related

Microsoft Access VBA code with Select SQL String and Where clause

I'm using Microsoft Access to develop a database app. An important feature the user would need is to automatically send an email update to all relevant stakeholders.
The problem is that I'm getting
Run-time error '3075' Syntax error in query expression.
Here it is below:
Set rs = db.OpenRecordset("SELECT StakeholderRegister.[StakeholderID], StakeholderRegister.[ProjectID], StakeholderRegister.[FirstName], StakeholderRegister.[LastName], StakeholderRegister.[EmailAddress] " & _
" FROM StakeholderRegister " & _
" WHERE (((StakeholderRegister.[ProjectID]=[Forms]![ChangeLog]![cboProjectID.Value])) ;")
Funny thing is that I created a query table on Access to create the relevant recordset and the turned on SQL view to copy the exact sql string that's above. That query works however it opens an Input Parameter box, whereas this code should be using the value typed into a forms text box as a matching criteria.
To use a variable as a parameter, do not include it within the quotes:
" WHERE StakeholderRegister.[ProjectID]=" & [Forms]![ChangeLog]![cboProjectID].[Value]
or just
" WHERE StakeholderRegister.ProjectID=" & Forms!ChangeLog!cboProjectID.Value
Note: You really only need the square brackets when there is something like a space in the name, which is not the best practice anyway.
I also took the liberty to remove the parentheses, as they are not needed in such a simple WHERE clause, and can cause more trouble than they are worth.
Try,
Dim strSQL As String
strSQL = "SELECT StakeholderRegister.[StakeholderID], StakeholderRegister.[ProjectID], StakeholderRegister.[FirstName], StakeholderRegister.[LastName], StakeholderRegister.[EmailAddress] " & _
" FROM StakeholderRegister " & _
" WHERE StakeholderRegister.[ProjectID]=" & [Forms]![ChangeLog]![cboProjectID].Value & " ;"
Set rs = Db.OpenRecordset(strSQL)
if [ProjectID] field type is text then
Dim strSQL As String
strSQL = "SELECT StakeholderRegister.[StakeholderID], StakeholderRegister.[ProjectID], StakeholderRegister.[FirstName], StakeholderRegister.[LastName], StakeholderRegister.[EmailAddress] " & _
" FROM StakeholderRegister " & _
" WHERE StakeholderRegister.[ProjectID]='" & [Forms]![ChangeLog]![cboProjectID].Value & "' ;"
Set rs = Db.OpenRecordset(strSQL)

VBA to query field contents in CSV

I'm struggling with ADO connections/recordsets.
My problem statement is: a function that will return the first value of a chosen field, in a chosen .csv file.
I am doing this to identify variably-named .csv files before adding the data to the relevant tables in a database. I am making the assumption that this field is always present and that either it is consistent throughout the file, or only relevant ones are grouped (this is controlled higher up the chain and is certain enough).
My code is being run as part of a module in an MS Access database:
Public Function GetFirstItem(File As Scripting.File, Field As String)
Dim Conn As ADODB.Connection, Recordset As ADODB.Recordset, SQL As String
Set Conn = New ADODB.Connection
Set Recordset = New ADODB.Recordset
'Microsoft.ACE.OLEDB.16.0 / Microsoft.Jet.OLEDB.4.0
Conn.ConnectionString = "Provider=Microsoft.ACE.OLEDB.16.0;Data Source=""" & File.ParentFolder & _
"""; Extended Properties=""text;HDR=Yes;FMT=Delimited;"";"
SQL = "SELECT " & Field & " FROM """ & File.Name & """ LIMIT 1"
Debug.Print Conn.ConnectionString
Debug.Print SQL
Conn.Open
Recordset.Source = SQL
Recordset.ActiveConnection = Conn.ConnectionString
Recordset.Open
Recordset.MoveFirst
'GetFirstItem = Recordset!Questionnaire
Recordset.Close
Conn.Close
Set Recordset = Nothing
Set Conn = Nothing
End Function
ConnectionString = Provider=Microsoft.ACE.OLEDB.16.0;Data Source="D:\Documents\Jobs\TestPath"; Extended Properties="text;HDR=Yes;FMT=Delimited;";
Field = Questionnaire
SQL = SELECT Questionnaire FROM "test.csv" LIMIT 1
I get an error on Recordset.Open of:
This may be (is probably) down to a complete lack of understanding of how ADO connections/recordsets work. I have tried sans-quotes and it complains about a malformed FROM expression. Additionally, once this hurdle is overcome I am unsure of the syntax of how to return the result of my query. If there is a better way of doing this I am all ears!
Thanks.
In Access you don't need ADO library to query a CSV file:
Public Function GetFirstItem(File As Scripting.File, Field As String) As String
Dim RS As DAO.Recordset, SQL As String
SQL = "SELECT TOP 1 [" & Field & "]" _
& " FROM [" & File.Name & "]" _
& " IN '" & File.ParentFolder & "'[Text;FMT=CSVDelimited;HDR=Yes];"
Debug.Print SQL
Set RS = CurrentDb.OpenRecordset(SQL)
GetFirstItem = RS(0)
RS.Close
Set RS = Nothing
End Function
Usage:
?GetFirstItem(CreateObject("Scripting.FileSystemObject").getfile("c:\path\to\your\file.csv"), "your field")

ACCESS/SQL - Too Few Parameters

I am using the code below to create a new record in the "transactions table" the second line of the insert statement is throwing an error: Too few parameters. I have double checked and all of the field names are correct. What else could cause this type of error?
' Modify this line to include the path to Northwind
' on your computer.
Set dbs = CurrentDb
Dim vblCustomerID As String
Dim vblMealType As String
Dim Charge As Currency
Dim vblDate As String
vblDate = Format(Date, "yyyy-mm-dd")
txtCustomerID.SetFocus
vblCustomerID = txtCustomerID.Text
txtMealType.SetFocus
vblMealType = txtMealType.Text
txtCharge.SetFocus
vblCharge = txtCharge.Text
dbs.Execute "INSERT INTO dbo_Transactions" _
& "(CustomerID, MealID, TransactionAmount, TransactionDate) VALUES " _
& "(" & vblCustomerID & ", " & vblMealType & ", " & vblCharge & ", " & vblDate & ");"
dbs.Close
As others have suggested, using a parameterized query is a much better way of doing what you're attempting to do. Try something like this:
Dim qdf As DAO.QueryDef
Set qdf = dbs.CreateQueryDef("", _
"PARAMETERS prmCustomerID Long, prmMealID Long, prmTransactionAmount Currency, prmTransactionDate DateTime;" & _
"INSERT INTO dbo_Transactions (CustomerID, MealID, TransactionAmount, TransactionDate) " & _
"VALUES ([prmCustomerID], [prmMealID], [prmTransactionAmount], [prmTransactionDate]) ")
qdf!prmCustomerID = txtCustomerID.Value
qdf!prmMealID = txtMealType.Value
qdf!prmTransactionAmount = txtCharge.Value
qdf!prmTransactionDate = Date()
qdf.Execute dbFailOnError
Set qdf = nothing
Do any of the text fields you're loading into your vbl fields contain special characters like these?
, ' "
All of those in a text field in a perfectly good SQL Insert command could screw things up, I bet that's what happening here.
It would be better if you actually use parameters here to, rather than loading the text in textboxes directly into your SQL queries, since you're opening yourself up to SQL Injections. What if someone types
"; Drop Table dbo_Transactions;
in one of your textboxes and you run this query? Your database is then totally screwed up because someone just deleted one of your tables.
A few links to info on using Parameters to prevent this issue, which I'll bet will also fix the too few parameters issue you're having.
http://forums.asp.net/t/886691.aspx
http://sqlmag.com/blog/t-sql-parameters-and-variables-basics-and-best-practices

Querying data from database with vb6 and ms access with adodb

I want to select and view my data in the database, but it’s proving to be a challenge. Any advice on where I could be missing it? If I run this code even when the select criteria is met, it always returns search failed. Any help?
If txtSun.Text = "SUN" Then
Set rst = New ADODB.Recordset
Dim sSql As String
sSql = "SELECT * FROM SundryProduct WHERE ProdCont='" & txt_con_code.Text & "'"
rst.Open sSql, Cnn, adOpenForwardOnly, , adCmdText
'rst.Open "SELECT * FROM SundryProduct WHERE ProdCont='" & txt_con_code.Text & "' ", Cnn, adOpenForwardOnly, , adCmdText
If rst.EOF Then
MsgBox ("SEARCH FAILED")
Else
MsgBox ("QUANTITY ORDERED " & rst!QuantityOrdered & vbCrLf & " My Load Number is " & rst!LoadNumber)
End If
End If
I am trying to find out if there is a record with a matching ProdCont value in the database, but since I was still trying to make this code work in the first place I have only put messageboxes in the code. I have even tried putting in an actual value that I know exists in the database but it still returns the search failed messagebox even though I know the value exists in the database.
If rst.EOF = True Then '----> here
MsgBox ("SEARCH FAILED")
Else
MsgBox ("QUANTITY ORDERED " & rst!QuantityOrdered & vbCrLf & " My Load Number is " & rst!LoadNumber)
End If
What happens is you try to just run a simple query i.e. select * from SundryProduct?
I would start with that and build on it to eliminate the possibilty of coding/syntax causing the error message

Improve asp script performance that takes 3+ minutes to run

I use an SQL statement to remove records that exist on another database but this takes a very long time.
Is there any other alternative to the code below that can be faster? Database is Access.
email_DB.mdb is from where I want to remove the email addresses that exist on the other database (table Newsletter_Subscribers)
customers.mdb is the other database (table Customers)
SQLRemoveDupes = "DELETE FROM Newsletter_Subscribers WHERE EXISTS (select * from [" & strDBPath & "Customers].Customers " _
& "where Subscriber_Email = Email or Subscriber_Email = EmailO)"
NewsletterConn = "Driver={Microsoft Access Driver (*.mdb)};DBQ=" & strDBPath & "email_DB.mdb"
Set MM_editCmd = Server.CreateObject("ADODB.Command")
MM_editCmd.ActiveConnection = NewsletterConn
MM_editCmd.CommandText = SQLRemoveDupes
MM_editCmd.Execute
MM_editCmd.ActiveConnection.Close
Set MM_editCmd = Nothing
EDIT: Tried the SQL below from one of the answers but I keep getting an error when running it:
SQL: DELETE FROM Newsletter_Subscribers WHERE CustID IN (select CustID from [" & strDBPath & "Customers].Customers where Subscriber_Email = Email or Subscriber_Email = EmailO)
I get a "Too few parameters. Expected 1." error message on the Execute line.
I would use WHERE Subscriber_Email IN (Email, Email0) as the WHERE clause
SQLRemoveDupes = "DELETE FROM Newsletter_Subscribers WHERE EXISTS " & _
(select * from [" & strDBPath & "Customers].Customers where Subscriber_Email IN (Email, EmailO)"
I have found from experience that using an OR predicate in a WHERE clause can be detrimental in terms of performance because SQL will have to evaluate each clause separately, and it might decide to ignore indexes and use a table scan. Sometime it can be better to split it into two separate statements. (I have to admit I am thinking in terms of SQL Server here, but the same may apply to Access)
"DELETE FROM Newsletter_Subscribers WHERE EXISTS " & _
(select * from [" & strDBPath & "Customers].Customers where Subscriber_Email = Email)"
"DELETE FROM Newsletter_Subscribers WHERE EXISTS " & _
(select * from [" & strDBPath & "Customers].Customers where Subscriber_Email = EmailO)"
Assuming there's an ID-column present in the Customers table, the following change in SQL should give better performance:
"DELETE FROM Newsletter_Subscribers WHERE ID IN (select ID from [" & strDBPath & "Customers].Customers where Subscriber_Email = Email or Subscriber_Email = EmailO)"
PS. The ideal solution (judging from the column names) would be to redesign the tables and code logic of inserting emails in the first place. DS
Try adding an Access Querydef and calling that.
It sounds like you do not have an index on the subscriber_enail field. This forces a table scan ( or several). Add an index on this field and you should see significant improvement.
I would have coded the query
DELETE FROM Newsletter_Subscribers where (Subscriber_Email = Email or Subscriber_Email = EMail0)
I would try splitting this into two separate statements with separate database connections.
First, fetch the list of email addresses or IDs in the first database (as a string).
Second, construct a WHERE NOT IN statement and run it on the second database.
I would imagine this would be much faster as it does not have to interoperate between the two databases. The only possible issue would be if there are thousands of records in the first database and you hit the maximum length of a sql query string (whatever that is).
Here are some useful functions for this:
function GetDelimitedRecordString(sql, recordDelimiter)
dim rs, str
set rs = db.execute(sql)
if rs.eof then
str = ""
else
str = rs.GetString(,,,recordDelimiter)
str = mid(str, 1, len(str)-len(recordDelimiter))
end if
rs.close
set rs = nothing
GetDelimitedRecordString = str
end function
function FmtSqlList(commaDelimitedStringOrArray)
' converts a string of the format "red, yellow, blue" to "'red', 'yellow', 'blue'"
' useful for taking input from an html form post (eg a multi-select box or checkbox group) and using it in a SQL WHERE IN clause
' prevents sql injection
dim result:result = ""
dim arr, str
if isArray(commaDelimitedStringOrArray) then
arr = commaDelimitedStringOrArray
else
arr = split(commaDelimitedStringOrArray, ",")
end if
for each str in arr
if result<>"" then result = result & ", "
result = result & "'" & trim(replace(str&"","'","''")) & "'"
next
FmtSqlList = result
end function