sql where statement with range: how to separate by a comma - vba

I'm new to VBA and I'm trying to pull info from a range of cells that will be plugged into the "where" clause of a sql statement. Since it is a where clause, I need to separate the variables with a comma. For example, my code is:
Sub loop()
AB=2
BC=9
Set IDX = Sheets("Sheet2).Cells(AB,BC)
Dim cn as ADODB.Connection
Dim rs as ADODB.Recordset
cn.ConnectionString=PORRI;Trusted_Connection=Yes;APP=Microsoft Office 2010; DATABASE=LKMJ_intm"
cn.open
set rs= New ADODB.Recordset
rs.ActiveConnection=cn
rs.OPEN "SELECT DATE, ID, NAME, INFO FROM xxx WHERE ID in ('"& IDX &")"
r=1
Do While Not rs.EOF
Cells (r,1) = rs.Fields(0)
Cells (r,2) = rs.Fields(1)
Cells (r,3) = rs.Fields(2)
Cells (r,4) = rs.Fields(3)
Cells (r,5) = rs.Fields(4)
r=r+1
Loop
End Sub
Right now, I can't create a range that will work in the where statement without the comma.
Thanks!

Assuming this piece of code works per one id:
rs.OPEN "SELECT DATE, ID, NAME, INFO FROM xxx WHERE ID in ('"& IDX &")"
What you wish to do is make sure that IDX would be translated to a list of codes, for example (1,2,3) or ('a', 'b', 'c').
If the list of codes is a list of integers I highly recommend getting the list of codes like this
rs.OPEN "SELECT DATE, ID, NAME, INFO FROM xxx WHERE ID in ('"& IDX &"," & IDB & ")"
(whatever IDB is)
and if it's a list of strings and you need to put them inside single quotes parameters:
rs.OPEN "SELECT DATE, ID, NAME, INFO FROM xxx WHERE ID in ('"& IDX &"','" & IDB & "')"
This is all very confusing with the quotes and single quotes! Luckily the editor here is smart enough to tell me when I got the code wrong :)

Related

Split multi-value field into different rows using SQL in Microsoft Access

I’ve got a simple table in Microsoft Access that looks like this:
Primary Key
Applications List
123
<Value>|<Value>,<Value>|<Value>
456
<Value>|<Value>,<Value>|<Value>
I need to break out the list of applications into separate rows using the “,” as a delimiter so the end result is a table that looks like this:
Primary Key
Applications List
123
<Value>|<Value>
123
<Value>|<Value>
456
<Value>|<Value>
456
<Value>|<Value>
I’ve tried using the Split function but can’t figure out how to split on the “,” and output the results to a different row like the second table above. I would greatly appreciate your help figuring this one out. Thanks so much!
If you can put a limit on the length of Application List you can build a number table
CREATE TABLE NumTable (ID integer)
up-front with as many rows as there are characters in the longest Application List. Assuming that the longest [Application List] is 1000 characters long, ID=1, ID=2, ..., ID=1000), and then use something like this:
select T.[Primary Key],
mid(T.AL,NT.ID+1
, iif(instr(NT.ID+1,t.AL,',')>0
, instr(NT.ID+1,t.AL,',')-1-nt.ID,1000)) as
from (select [Primary Key], ',' & [Application List] as AL from tblYourTable) as T
inner join
NumTable as NT
on mid(t.AL,NT.ID,1)=','
You can build the number table in EXCEL and paste it; or write a little VBA routine, or even create it dynamically (*).
I can't imagine it performing very well if the volume is high.
Let us know how you proceed!
(*)Generate numbers 1 to 1000000 in MS Access using SQL
If you setup a Table1 and a Table2 with the same field names, you can run this function to do it for you - it basically loops through Table1 records, splits the Applications List into multiple fields and then inserts new records for each field.
Public Sub SplitDataIntoNewTable()
Const DATA_SEPARATOR As String = ","
Dim qdf As DAO.QueryDef
Dim rsOld As DAO.Recordset
Dim rsNew As DAO.Recordset
Dim i As Integer
Dim lngNumAdded As Long
Dim lngKey As Long
Dim strList As String
Dim strNewList As String
Dim varLists As Variant
Set rsOld = CurrentDb.OpenRecordset("Table1", dbOpenDynaset, dbReadOnly)
Set rsNew = CurrentDb.OpenRecordset("Table2", dbOpenDynaset)
With rsOld
While Not .EOF
lngKey = ![Primary Key]
strList = ![Applications List]
varLists = Split(strList, DATA_SEPARATOR)
With rsNew
For i = LBound(varLists) To UBound(varLists)
' Add new record for every list split out of old data
.AddNew
' Note that this CANNOT actually be defined as a PRIMARY KEY - it will have duplicates
![Primary Key] = lngKey
![Applications List] = varLists(i)
lngNumAdded = lngNumAdded + 1
.Update
Next i
End With
.MoveNext
Wend
rsNew.Close
.Close
End With
MsgBox "Added " & lngNumAdded & " New Records"
Set rsOld = Nothing
Set rsNew = Nothing
End Sub
For example I had Table1 look like this:
And resulting Table2 ended up like this

Adding a complete row to a table from a DAO.Recordset

I have an access table containing product information with 100 Columns (it is a bought system so not of my creation). I want to be able to copy a row and insert it as a new row with 3 fields updated. I am using VBA on an access DB.
I am selecting the row I want to copy (Select Product.* FROM .....) and putting it into a DAO.Recordset. This works fine. I then want to insert this data back into the table as a new row with all the same data apart from the Product ID (key) and the Product short description.
As there is 100 columns I am trying to avoid typing in all the column names and assigning the values individually. Is there a way to insert from a DAO.Recordset so I can avoid typing in all the columns? if not is there another way to avoid typing in all the columns and all the values? It would save me a very big insert statement!
Many thanks
Tony
You can loop the Fields collection of the recordset to do this.
This approach may be more maintainable then a giant INSERT statement if the table structure changes from time to time.
If the table is static, I would rather use a saved INSERT query with parameters for the columns that are modified.
Sub CopyProductRow()
Dim rsSrc As DAO.Recordset
Dim rsTgt As DAO.Recordset
Dim fld As DAO.Field
Dim sFld As String
Set rsSrc = CurrentDb.OpenRecordset("SELECT * FROM Products WHERE ProductID = 4711", dbOpenSnapshot)
Set rsTgt = CurrentDb.OpenRecordset("Products", dbOpenDynaset, dbAppendOnly)
rsTgt.AddNew
For Each fld In rsSrc.Fields
sFld = fld.Name
Select Case sFld
' special cases
Case "ProductID": rsTgt(sFld).Value = GetNewProductID()
' or if ProductID is AutoNumber, don't assign anything
Case "ProductDesc": rsTgt(sFld).Value = "my new description"
' all other field values are simply copied
Case Else: rsTgt(sFld).Value = fld.Value
End Select
Next fld
rsTgt.Update
rsTgt.Close
rsSrc.Close
End Sub
If you use a form where you select the record to copy, you can also use the RecordsetClone:
Copy selected record to new record
If you are trying to insert it back into the same table then you can do it without puling it into a recordset at all.
You just need to write the correct SQL query and execute it.
The data that you select will be the recordset that you are already pulling with the updated values.
For instance:
INSERT INTO tableX(field1, productId, productDesc)
SELECT field1, 777 as productId, "NewString" as productDesc
FROM tableX
WHERE productId=7
Another approach which I mentioned in comments would be to loop through each of the fields to build your string used as the SQL command, the execution of this would be a lot faster then processing record by record. (such as inserting a new product for each order in an orders table where another product has already been ordered, which could have 10s of 1000s of orders)
'Edited the code supplied by another response above'
Sub CopyProductRow()
Dim sFld, iField, sqlQuery As String
i= "INSERT INTO products("
s= "SELECT "
w= "WHERE ProductID = 7"
For Each fld In rsSrc.Fields
Select Case fld.Name 'Check the field name'
' special cases'
Case "ProductID": 'If field is Product ID'
iFld = "777 as ProductID" '777 will be the product id returned by the select query (therefore inserted)'
Case "ProductDesc": 'If field is Product Description '
'The description below will be selected / inserted instead.'
iFld = "'New Product Description' as ProductDesc"
Case Else:
iFld = fld.Name 'No change just select the field and insert as is.'
End Select
i = i & ", " & fld.Name
s = s & ", " & iFld
Next fld
'Build complete query (removing the first comma in the select statement)'
sqlQuery = i & ") " & replace(s, "SELECT ,", "SELECT ") & " " &w
'Resulting in a string such as the following'
'INSERT INTO products(field1,field2, productId, productDesc, field5, ...)'
'SELECT field1, field2, 777 as ProductID, 'New Product Description' as ProductDesc, field5, ...'
'FROM tableX'
'WHERE productId=7'
'Print the query to immediate window
Debug.print sqlQuery
'Execute the query'
currentdb.execute sqlQuery
End Sub

Excel to VB: Can't read the zero behind

I'm doing a connection with excel and I have a problem when I try to use an ID that have 0 behind...
I'm using a ListBox and add the IDs from the excel's worksheet as items. IDs have 9 numbers, like "123456789" or "098765430". So that I remove the last 4 characters to search the IDs with the same 5 numbers and add in another ListBox. It works fine, except with the codes with 0 (zero) behind.
Dim ConnectionString As New OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0; Data Source=" & Application.StartupPath & "\Tabela_Precos.xlsx; Extended Properties=Excel 12.0;")
ConnectionString.Open()
Dim ds As New DataSet
Dim dt As New DataTable
ds.Tables.Add(dt)
Dim da
For i = 0 To Form1.ListBox1.Items.Count - 1
Dim str As String = Compras.ListBox1.Items(i).ToString
Dim prod As String = str.Remove(str.Length - 4)
da = New OleDbDataAdapter("SELECT * FROM [Sheet1$] WHERE ID like '%" & prod & "%'", ConnectionString)
ListBox1.Items.Add(dt.Rows(i).Item(0))
Next
Your Excel file has the ID column entered as integer values, but is formatted for left-zero padding to present as a nine character field. Your Excel db connection is reading the values as numbers (type Double, even-though they are integers). Your original select statement is implicitly convert ID to a string for the Like comparison; however, this conversion does not now you want left-zero padding. To use this type of comparison, you need to format ID yourself.
Select * From [sheet1$] Where (Format([ID], ""000000000"") Like '" & prod & "%')"
As you have indicated in the comments above, this works. However, it is not the most efficient in terms of speed. Since ID is numeric, it should be faster to do a numeric comparison. You have already defined a String variable named prod and the following solution uses that variable to prepare a numeric value for use in constructing an alternate select based on your criteria.
Dim prodNum As Int32 = Int32.Parse(prod) * 10000I
Then the Select statement would become:
"Select * From [sheet1$] Where ((([ID]\10000) * 10000)=" & prodNum.ToString & ")"
These examples use a concatenated select statement, and ideally you would not do it this way, but rather use a parameterized statement with replacement values. I'll leave that exercise up to you to perform.

MS Access Looping a union query through all columns?

Is it possible to simplify the following into a loop?
SELECT ID as ColHead, "Field1" AS RowHead, Field1 AS TheVal
FROM `master`
UNION
SELECT ID, "Field2",Field2
FROM `master`
UNION
SELECT ID, "Field3",Field3
FROM `master`
UNION
...
SELECT ID, "Field90",Field90
FROM `master`
There are 90 fields. Before I start writing this by hand, is there a way to simplify the process?
If you build that massive UNION query, consider UNION ALL instead.
UNION ALL will return all rows from each SELECT piece, including duplicate rows. With just UNION, the db engine returns only distinct rows. However ensuring distinct rows imposes a greater burden on the engine.
Use UNION if you require only distinct rows. Use UNION ALL if you can accept duplicate rows. Also use UNION ALL when the candidate rows can not include duplicates to begin with.
If that ID field is the master primary key, or if you have another unique constraint (index) on ID, the candidate rows will already be unique. If that is your situation, use UNION ALL to make that big query easier on the db engine.
Actually I'm apprehensive about trying to UNION (or UNION ALL) 89 SELECTs. I've never attempted such a huge SQL statement. If you want to try, I'll offer another approach.
I created a VBA function to create the SQL statement. It reads the field names from the TableDef and loops through those names to add a SELECT piece for each field name.
Here is an Immediate window session where I tested the function in Access 2007. My master table includes only 4 fields: ID; fld1; fld2; and fld3.
? BuildUnionStatement
SELECT ID as ColHead, 'fld1' AS RowHead, [fld1] AS TheVal
FROM [master]
UNION ALL
SELECT ID as ColHead, 'fld2' AS RowHead, [fld2] AS TheVal
FROM [master]
UNION ALL
SELECT ID as ColHead, 'fld3' AS RowHead, [fld3] AS TheVal
FROM [master]
I don't know what you intend to do with the query after you create it. But a function to create the SQL offers flexibility. You could use the function's output to open a recordset, save it as a QueryDef, for the record source of a form or report, etc.
Public Function BuildUnionStatement() As String
Const cstrTable As String = "master"
Dim db As DAO.database
Dim fld As DAO.Field
Dim tdf As DAO.TableDef
Dim strPattern As String
Dim strSql As String
'strPattern = vbCrLf & "UNION" & vbCrLf &
strPattern = vbCrLf & "UNION ALL" & vbCrLf & _
"SELECT ID as ColHead, " & _
"'FLDNAME' AS RowHead, " & _
"[FLDNAME] AS TheVal" & vbCrLf & _
"FROM [" & cstrTable & "]"
Set db = CurrentDb
Set tdf = db.TableDefs(cstrTable)
For Each fld In tdf.Fields
If fld.Name <> "ID" Then
strSql = strSql & Replace(strPattern, _
"FLDNAME", fld.Name)
End If
Next
Set fld = Nothing
Set tdf = Nothing
Set db = Nothing
'BuildUnionStatement = Mid(strSql, 10) ' UNION
BuildUnionStatement = Mid(strSql, 14) ' UNION ALL
End Function
After you save that function in a module, open the Immediate window (Ctrl+g). To execute the function, type this and press Enter
? BuildUnionStatement
Copy the text it returns, create a new query and switch to SQL View, then paste in the copied text.
Since that gave you too much text to copy from the Immediate window, create a new query --- any query will work. Then assign the function's output to the query's SQL property. Do this in the Immediate window ...
CurrentDb.QueryDefs("YourQueryNameHere").SQL = BuildUnionStatement

MS access SELECT INTO in vba

I'm having some issues with some functionality of my application. There is a particular instance where I have an instance of a 'pending class' on a form for an administrator to review. The form is populated with students associated with this pending class. After their grades are finished, I have a button at the footer that will delete this class from my 'pending' table and add the grades to all of the students. This works.
However, I want to essentially copy this pending class, which just has the class name, date, and teacher to a completed class table before it's deleted from pending. Since no data about this class other than the primary key(class number) persists throughout this form, I can't populate the other fields(class name, date) of the row into my completed class table.
I am trying a "SELECT INTO" operation in VBA to get these values. It's going like this:
dim cname as String
dim classdate as Date
dim pid as integer
dim teacher as String
dim qry as String
pid = [Forms]![frmClasses]![txtID]
qry = "Select className INTO cname FROM tblPending WHERE tblPending.id = " & " ' " & pid & " ' " & ";"
db.execute qry
debug.print qry
debug.print cname
From here, I do the same operations for each other variable, build my INSERT query, and execute it. The problem is-- my select into's are not working. Debug.print shows that the local variables were never initialized from the SELECT INTO statement. Any thoughts?
First, having all classes in one table and just setting a "NotPending" or "Completed" column would be better.
Having two identical tables for classes and moving values from one into the other to indicate status changes is bad database design.
If you really need to do this by using two tables and copying rows, then you need an INSERT INTO query (and not SELECT INTO), as already mentioned by Remou in the comments, because SELECT INTO creates a new table (or overwrites an existing one with the same name, if already there).
The syntax for INSERT INTO looks like this:
INSERT INTO CompletedClassTable (ClassName, Teacher)
SELECT ClassName, Teacher FROM tblPending WHERE id = 123
And finally, you asked this in a comment:
So SELECT INTO is completely different in Access than Oracle? In Oracle and PL/SQL, you can select a row into a variable OR a table. In Access can you not select into a variable?
To load a row into a variable, you need to use a Recordset.
Example code to load your query into a Recordset and output the ClassName field:
Dim RS As DAO.Recordset
Set RS = CurrentDb.OpenRecordset("SELECT * FROM tblPending WHERE id = 123")
If Not RS.EOF Then
Debug.Print RS("classname")
End If
RS.Close
Set RS = Nothing
Seems you want to retrieve a text value, className, from tblPending where tblPending.id matches the value found in your text box, txtID, and store that text value in a string variable named cname.
If that interpretation is correct, you needn't bother with a query and recordset. Just use the DLookup Function to retrieve the value, similar to this untested code sample.
Dim cname As String
Dim pid As Integer
Dim strCriteria As String
pid = [Forms]![frmClasses]![txtID]
strCriteria = "id = " & pid
cname = Nz(DLookup("className", "tblPending", strCriteria), vbNullString)
Debug.Print "cname: '" & cname & "'"
Notes:
I assumed the data type of the id field in tblPending is numeric. If it is actually text data type, change strCriteria like this:
strCriteria = "id = '" & pid & "'"
DLookup() returns Null if no match found. Since we are assigning the function's return value to a string variable, I used Nz() to convert Null to an empty string. Alternatively, you could declare cname As Variant (so that it can accept a Null value) and get rid of Nz().