SQL for MSACCESS how to implement character delimited data hierarchy? - sql

Say, there's a flattened sequential hierarchy table with a special character "+" denoting hierarchy level
hr_table
ID FIELD1 My irrelevant comments
----------------
1 ASSETS No pluses - it means level0 hierarchy
2 +ASSETS_01 Level1 hierarchy
3 ++345667654 Level2 hierarchy
4 ++563255512 Level2 hierarchy
5 ...
Is there a way to create field structure in MSACCESS using SQL? I am trying to structure final data as follows:
final_data_table
ID LEVEL0 LEVEL1 LEVEL2 ...
-------------------------------------------
1 ASSETS ASSETS_01 345667654
2 ASSETS ASSETS_01 563255512
Any/all help greatly appreciated!

Curiosity got the better of me so I explored query approach. I resorted to using domain aggregate functions. Be aware domain aggregate functions can perform slowly with large datasets. However, consider:
Query1:
SELECT hr_table.ID, IIf([Field1] Like "+*",Left([Field1],InStrRev([Field1],"+")),0) AS Prefix, IIf([Field1] Like "+*",Null,[Field1]) AS Asset1, IIf(InStrRev([Field1],"+")=1,Mid([Field1],2),Null) AS Asset2, IIf([Field1] Like "++*",Mid([Field1],InStrRev([Field1],"+")+1),Null) AS Data
FROM hr_table;
Query2:
SELECT Query1.ID, Query1.Prefix, DMax("Asset1","Query1","ID<=" & [ID]) AS A1, DMax("Asset2","Query1","ID<=" & [ID]) AS A2, Query1.Data
FROM Query1
WHERE ((Not (Query1.Data) Is Null));
Query3:
SELECT Query2.Prefix, Query2.A1, Query2.A2, Query2.Data, DCount("*","Query2","A1='" & [A1] & "' AND A2='" & [A2] & "' AND Prefix = '" & [Prefix] & "' AND ID<=" & [ID]) AS GrpSeq
FROM Query2;
Query4:
TRANSFORM Max(Query3.Data) AS MaxOfData
SELECT Query3.A1, Query3.A2, Query3.GrpSeq
FROM Query3
GROUP BY Query3.A1, Query3.A2, Query3.GrpSeq
PIVOT Query3.Prefix;
I am definitely not sure about treatment of Level3 and beyond. Could be VBA will be only way to resolve.

The following code has been tested and works with the data structure you have mentioned. It is currently set to handle up to 10 levels, but that can be easily changed. The tricky part was to NOT write out the record until you have all levels for that ONE row (New row starts with different level1 value, or when more than one level-n value provided). The final row is written after eof of input.
Option Compare Database
Option Explicit
Function Parse_Fields()
Dim dbs As DAO.Database
Dim rsIN As DAO.recordSet
Dim rsOUT As DAO.recordSet
Dim i As Integer
Dim iPlus As Integer
Dim aLevels(10)
Dim bAdding As Boolean
Set dbs = CurrentDb
Set rsIN = dbs.OpenRecordset("hr_table")
Set rsOUT = dbs.OpenRecordset("final_data_table")
bAdding = False
Do While Not rsIN.EOF
'Debug.Print "Input: " & rsIN!field1
If left(rsIN!field1, 1) <> "+" Then
' Check if not first time thru... if not, write prior levels..
If bAdding = True Then
rsOUT.Update
End If
iPlus = 0
rsOUT.AddNew
rsOUT!Level0 = rsIN!field1
bAdding = True
' Don't issue the .Update yet! Wait until another Level0 or EOF.
Else
For iPlus = 1 To 10 ' Change code if more than ten levels
If Mid(rsIN!field1, iPlus, 1) <> "+" Then Exit For
Next iPlus
' Check if same level as previous! If so, do NOT overlay!
If Not IsNull(rsOUT.Fields(iPlus)) Then
For i = 1 To iPlus - 1 ' Save the proper levels for the new record.
aLevels(i) = rsOUT.Fields(i)
Next i
rsOUT.Update ' Need to write out record.
rsOUT.AddNew
For i = 1 To iPlus ' Populate the new record with prior levels
rsOUT.Fields(i) = aLevels(i)
Next i
rsOUT.Fields(iPlus) = Mid(rsIN!field1, iPlus) ' Add the new level
Else
rsOUT.Fields(iPlus) = Mid(rsIN!field1, iPlus)
End If
End If
rsIN.MoveNext ' Get next input
Loop
' Need to write out the final record we have beenbuilding!
rsOUT.Update
rsIN.Close
rsOUT.Close
Set rsIN = Nothing
Set rsOUT = Nothing
Set dbs = Nothing
Debug.Print "FINISHED!!"
End Function

Related

Adding records based on value from other records

I'm going to try this again because I'm so lost..: I have an Access database and the backend contains the tables:
tblStat
groupID
Userid
complex
open
new account
Shipped
not-shipped
code
A
123
Yes
No
Yes
869
B
147
No
Yes
no
936
And tblCode
uniqueid
codetype
code
A,yes,No,Yes
shipped
869
B,No,Yes,No
not-Shipped
936
When I upload a report, it populates the tblStats and using the groupid, complex, open and new account records to make a uniqueid and later adds the code to tblStats. Based on the uniqueID, it can be shipped or not-shipped. The old reports we used contained shipped or not-shipped number values, but with the new reports, I will need to use codetype and code record to reference the tblStat for each order and determine whether the order is shipped or not, and add 1 to tblStats.
I want to do something like
if tblCode.codetype = shipped and the code are the same then tblStat.shipped = 1 else if tblCode.Codetype = not-shipped and the codes are the same tblStat.not-shipped = 1
and then:
with recordset .shipped = shipped .not-shipped = notshipped end with
I just can't seem to figure it out..
I really hope I've provided enough info and cleraity this time around. If you need any code, let me know. thanks.
I cant get my head around for a solution purely in SQL, but in VBA, the method below does the following:
Gather records due to be updated from tblStat.
Loop and try to find a match in tblCode.
Update record accordingly.
There's a helper function to try to find the match in tblCode and an enum the function returns to make the code a bit cleaner and easier to read.
I'm pretty sure something better exists.
Private Enum ShipEnum
None = 0
Shipped
NotShipped
End Enum
Sub T()
On Error GoTo catch
'get records due to be updated
Dim r As DAO.Recordset
Set r = CurrentDb().OpenRecordset("SELECT groupID, complex, [open], [new account], Shipped, [not-shipped], code FROM tblStat WHERE Shipped Is Null AND [not-shipped] Is Null;", dbOpenDynaset)
If r.EOF Then
MsgBox "No records."
GoTo Leave
End If
r.MoveLast
r.MoveFirst
'loop and try to match each record with a record in tblCode
Dim idx As Long, uniqueId As String, shippedStatus As ShipEnum
For idx = 1 To r.RecordCount
'build the unique id for the record
uniqueId = r![groupID] & "," & r![Complex] & "," & r![Open] & "," & r![new account]
'get codetype
shippedStatus = CodeTypeByCriteria(uniqueId, r![Code])
'update if shipped or not-shipped
If shippedStatus <> ShipEnum.None Then
r.Edit
r![Shipped] = IIf(shippedStatus = ShipEnum.Shipped, 1, 0)
r![not-shipped] = IIf(shippedStatus = ShipEnum.NotShipped, 1, 0)
r.Update
End If
r.MoveNext
Next idx
'all good
MsgBox "Complete"
Leave:
If Not r Is Nothing Then r.Close
Exit Sub
catch:
MsgBox Err.Description, vbCritical
Resume Leave
End Sub
Private Function CodeTypeByCriteria(ByVal stringUniqueId As String, ByVal stringCode As String) As ShipEnum
With CurrentDb().OpenRecordset("SELECT codetype FROM tblCode WHERE StrConv(uniqueId, 2)='" & StrConv(stringUniqueId, vbLowerCase) & "' And code='" & stringCode & "'", dbOpenSnapshot)
If Not .EOF Then
Select Case ![codetype]
Case "shipped":
CodeTypeByCriteria = ShipEnum.Shipped
Case "not-shipped":
CodeTypeByCriteria = ShipEnum.NotShipped
Case Else:
Exit Function
End Select
End If
End With
End Function
If code column in both the tables have unique values and you haven't made mistake while writing code field's values for both tables in your question then these queries should work:
UPDATE tblStat JOIN tblCode ON tblStat.code = tblCode.code
SET shipped = 1
WHERE CONCAT(groupID, ',', complex, ',', open, ',', new_account) = uniqueid
AND codetype = 'shipped';
UPDATE tblStat JOIN tblCode ON tblStat.code = tblCode.code
SET not_shipped = 1
WHERE CONCAT(groupID, ',', complex, ',', open, ',', new_account) = uniqueid
AND codetype = 'not-shipped';
Edit:
These queries are according to MySQL. In VBA, there should be some other function or way - like joining string with + operator?

Get all possible childs/decendants of a parent

I searched for this a lot but couldn't fine anything that is usable for MS Access, I found solutions for SQL but the statements that are used are not allowed in access SQL.
So, in MS access 2019 I have a table tbContentList with an Id and ParentID. What I would like is to show all childs/decendants of a specific parent.
My table look like this:
If I want to show all the childs of Id 3, I would like to end up with the result:
Is this possible in MS access queries? It is possible with VBA but I think it's faster to do it with queries. Can anybody help me with this topic please?
the SQL equivalent:
https://www.codeproject.com/Articles/818694/SQL-Queries-to-Manage-Hierarchical-or-Parent-child
(All possible childs)
So, I was able to modify the logic from Gustav and make it suitable for my project. I put the parent result between a delimiter ";". This makes it easier to find the descendants of a specific ContentID in the query. Furthermore I had to handle Null values in the parent column since some of the ContentID are the beginning of the the tree.
Public Function GetParentIDs(ByVal lContentID As Long) As String
Static dbs As DAO.Database
Static tbl As DAO.TableDef
Static rst As DAO.Recordset
Dim strParents As String
If dbs Is Nothing Then
' For testing only.
' Replace with OpenDatabase of backend database file.
Set dbs = CurrentDb
Set tbl = dbs.TableDefs("tbContentList")
Set rst = dbs.OpenRecordset(tbl.Name, dbOpenTable)
End If
With rst
.Index = "PrimaryKey"
Do While lContentID > 0
.Seek "=", lContentID
If Not .NoMatch Then
lContentID = Nz(!ParentID.Value, 0)
If lContentID > 0 Then
strParents = ";" & CStr(lContentID) & strParents
Else
Exit Do
End If
Else
Exit Do
End If
Loop
' Leave recordset open.
' .Close
End With
' Don't terminate static objects.
' Set rst = Nothing
' Set tbl = Nothing
' Set dbs = Nothing
'Return value
If strParents = "" Then
GetParentIDs = ""
Else
GetParentIDs = strParents & ";"
End If
End Function
The query to get all Descendants from a specific ContentID. The 3 if for this example, this could be changed to another value.
SELECT tbContentList.[ContentID], tbContentList.[ParentID], tbContentList.[Item], GetParentIDs([ContentID]) AS Parents
FROM tbContentList
WHERE (((GetParentIDs([ContentID])) Like '*;3;*'));
Thanks for the help and putting me in the right direction.
You have several options. One, however, won't do and that is a recursive query using SQL only; Access can't be fooled and will claim about a circular reference. Your only chance is to create a query resolving a limited number of levels only, say, 8 or 10.
But you can cover the recursive call in a domain aggregate function like DLookup(). This is, however, very slow as DLookup() calling the query will run for each and every record. For more than some dozens of records this will most likely be unacceptable.
The fastest way, for an unlimited number of levels, I've found is to create a lookup function which walks the tree for each record. This can output either the level of the record or a compound key built by the key of the record and all keys above.
As the lookup function will use the same recordset for every call, you can make it static, and (for Jet) you can improve further by using Seek to locate the records.
Here's an example from a similar case which will give you an idea:
Public Function RecursiveLookup(ByVal lngID As Long) As String
Static dbs As DAO.Database
Static tbl As DAO.TableDef
Static rst As DAO.Recordset
Dim lngLevel As Long
Dim strAccount As String
If dbs Is Nothing Then
' For testing only.
' Replace with OpenDatabase of backend database file.
Set dbs = CurrentDb
Set tbl = dbs.TableDefs("tblAccount")
Set rst = dbs.OpenRecordset(tbl.Name, dbOpenTable)
End If
With rst
.Index = "PrimaryKey"
While lngID > 0
.Seek "=", lngID
If Not .NoMatch Then
lngLevel = lngLevel + 1
lngID = !MasterAccountFK.Value
If lngID > 0 Then
strAccount = str(!AccountID) & strAccount
End If
Else
lngID = 0
End If
Wend
' Leave recordset open.
' .Close
End With
' Don't terminate static objects.
' Set rst = Nothing
' Set tbl = Nothing
' Set dbs = Nothing
' Alternative expression for returning the level.
' (Adjust vartype of return value of function.)
' RecursiveLookup = lngLevel ' As Long
RecursiveLookup = strAccount
End Function
This assumes a table with a primary key ID and a foreign (master) key pointing to the parent record - and a top level record (not used) with a visible key (AccountID) of 0.
Now your tree will be nicely shown almost instantaneously using a query like this where Account will be the visible compound key:
SELECT
*, RecursiveLookup([ID]) AS Account
FROM
tblAccount
WHERE
(AccountID > 0)
ORDER BY
RecursiveLookup([ID]);

Can't update table fields depending on value on number

So I have a form where I can select an excel file, it'll make a table which is an exact copy of that file, and then it'll try to match fields from that table with a project table and update the matching fields. The issue is sometimes the projects field won't update. As an example the existing value is 1.0319. If my excel file has 1.026 it will not update. 1.026 does appear in the temp table. But if I change it to 1.016 in the excel it will update. Then if I change it back to 1.026, it will update. However if I change it to 1.0319, the original value, it won't update. It honestly has me baffled and I wonder if it's actually a fault in access or VB. Here's the code, I simplified it a bit by removing the other fields it tests for and the excel load as that works fine.
Dim sSQL As String
Dim db As Database
Dim recTemp, recProj As Recordset
Dim intUpdatedRecordCount As Integer
Dim bUpdatedRecord As Boolean
Dim sSelectedFieldsQuery As String
sSelectedFieldsQuery = "P_Ratio"
'Update Generator data with imported table
Set db = CurrentDb()
sSQL = "SELECT TempImpProjRes.Desc, TempImpProjRes.ElemName, TempImpProjRes.BusA, TempImpProjRes.ID, TempImpProjRes.ProjID, " & _
"TempImpProjRes.ElemID, " & sSelectedFieldsQuery & " FROM TempImpProjRes"
Set recTemp = db.OpenRecordset(sSQL, dbOpenDynaset, dbConsistent)
'begin to loop over imported data
If recTemp.RecordCount > 0 Then
recTemp.MoveFirst
Do While Not recTemp.EOF
sSQL = "SELECT Projects.ProjID, Projects.ElemID,"Projects.P_Ratio FROM Projects WHERE Projects.ProjID=" & recTemp!ProjID & " AND Projects.ElemID=" & recTemp!ElemID"
Set recProj = db.OpenRecordset(sSQL, dbOpenDynaset, dbConsistent)
intUpdatedRecordCount = 0
bUpdatedRecord = False
bUpdatedRecord = Not CDbl(Format(recProj!P_Ratio, "0.00")) = CDbl(Format(recTemp!P_Ratio, "0.00"))
intUpdatedRecordCount = intUpdatedRecordCount + BooleanToInt(bUpdatedRecord)
'if any field has been updated then we need to update the respective value in the Projects table
If intUpdatedRecordCount > 0 Then
recProj.Edit
recProj!P_Ratio = CDbl(Format(recTemp!P_Ratio, "0.0000"))
recProj!Updated = Date
recProj.Update
End If
recProj.Close
Set recProj = Nothing
recTemp.MoveNext
Loop
End If
recTemp.Close
db.Close
Set recTemp = Nothing
Set db = Nothing

String builder SELECT query when a variable has an apostrophe

A colleague of mine has created a program that reads a text file and assigns various values from it to variables that are used in SQL statements.
One of these variables, gsAccounts is a string variable.
Using a string builder, a SELECT statement is being built up with sql.append. At the end of the string, there is the following line:
sql.Append(" WHERE L.Account_Code IN(" & gsAccounts & ")"
The problem that I'm having is that sometimes, not always, gsAccounts (a list of account codes) may contain an account code with an apostrophe, so the query becomes
"WHERE L.Account_Code IN('test'123')"
when the account code is test'123
I have tried using double quotes to get around it in a "WHERE L.Account_Code IN("""" & gsAccounts & """")" way (using 4 and 6 " next to each other, but neither worked)
How can I get around this? The account_Code is the Primary Key in the table, so I can't just remove it as there are years worth of transactions and data connected to it.
I posted the following example here 10 years ago, almost to the day. (Oops! thought it was Jun 5 but it was Jan 5. 10.5 years then.)
Dim connection As New SqlConnection("connection string here")
Dim command As New SqlCommand
Dim query As New StringBuilder("SELECT * FROM MyTable")
Select Case Me.ListBox1.SelectedItems.Count
Case 1
'Only one item is selected so we only need one parameter.
query.Append(" WHERE MyColumn = #MyColumn")
command.Parameters.AddWithValue("#MyColumn", Me.ListBox1.SelectedItem)
Case Is > 1
'Multiple items are selected so include a parameter for each.
query.Append(" WHERE MyColumn IN (")
Dim paramName As String
For index As Integer = 0 To Me.ListBox1.SelectedItems.Count - 1 Step 1
'Name all parameters for the column with a numeric suffix.
paramName = "#MyColumn" & index
'Add a comma before all but the first value.
If index > 0 Then
query.Append(", ")
End If
'Append the placeholder to the SQL and add the parameter to the command
query.Append(paramName)
command.Parameters.AddWithValue(paramName, Me.ListBox1.SelectedItems(index))
Next index
query.Append(")")
End Select
command.CommandText = query.ToString()
command.Connection = connection
Single quotes can be "escaped" by making them double single quotes. E.g. ' becomes ''.
However this approach is generally not recommended due to the high risk of SQL injection - a very dangerous and prevalent issues. See: https://www.owasp.org/index.php/SQL_Injection
To avoid this most libraries will include some type of escaping mechanism including the use of things like prepared statements in the Java world. In the .net world this may be of use: https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlcommand.prepare(v=vs.110).aspx
If you only have one with a field, this is the easiest solution
Private Function gsAccountsConvert(ByVal gsAccounts As String)
Dim gsAccountsString As String = ""
Dim StringTemp
StringTemp = gsAccounts.Split(",")
Dim i As Integer
For i = 0 To UBound(StringTemp)
StringTemp(i) = StringTemp(i).ToString.Trim
If StringTemp(i) <> "" Then
If StringTemp(i).ToString.Substring(0, 1) = "'" Then
StringTemp(i) = """" & StringTemp(i).ToString.Substring(1, Len(StringTemp(i).ToString) - 2) & """"
End If
End If
If i <> UBound(StringTemp) Then
gsAccountsString = gsAccountsString & StringTemp(i).ToString.Replace("'", "''") & ","
Else
gsAccountsString = gsAccountsString & StringTemp(i).ToString.Replace("'", "''") & ""
End If
Next
gsAccountsString = gsAccountsString.Replace("""", "'")
Return gsAccountsString
End Function

MS Access VBA: Calculate Median without iterating through records

I am trying to find a quicker way to calculate my medians in Access. You can see the code below where it queries one item code at a time, sorts, and then calculates the median. Sometimes there are 600 item codes and those items can each have a 1000+ bases associated with it. The particular table I am working with has 150,000 total records for example and it is going really slow. Is there a better way to calculate each records median all at once as opposed to one item code at a time.
Function FIncPercentile(ByVal posCode As Single, ByVal k As Single, ByVal tbl As String) As Variant
Dim rstRec As Recordset
Dim db As Database
Dim n As Integer
Dim i As Single
Dim res, d1, d2 As Currency
' Create recordset from query
Set db = CurrentDb
Set rstRec = db.OpenRecordset("SELECT Co, Base " & _
"FROM " & tbl & " " & _
"WHERE Code = " & pos & " " & _
"ORDER BY Base ASC;")
' Skip if there are no matches
If IsNull(rstRec!base) Or rstRec.RecordCount = 0 Then
FBasePercentile = Null
Exit Function
End If
' Count records
rstRec.MoveLast
n = rstRec.RecordCount
rstRec.MoveFirst
' Calculate the index where k is the percentile
i = n * k
' Test the decimal and find value accordingly
If i = Int(i) Then
rstRec.Move i - 1
d1 = rstRec!base
rstRec.MoveNext
d2 = rstRec!base
FIncPercentile = (d1 + d2) / 2
Else
i = Round(i + 0.5, 0)
rstRec.Move i - 1
FIncPercentile = rstRec!base
End If
End Function
There is no Median function in Access. Excel has one but I believe it's limited to 30 numbers, so even if you wanted to try using an Automation function, I don't believe it would work for your case.
I think you may see a noticeable speed increase by fine tuning your function and by letting Microsoft's Jet Engine pre-compile your query.
Make sure you have the Base and Code fields indexed in your table(s)
Create a Parameter Query with Code having the criteria parameter
[What Code]
Optimize your function with Recordset WITH construct, declared
variables and matched field types (Code = Long Integer???)
Time it before and after all these changes and see if there's any noticeable difference
I corrected a couple typos that may not be typos - and I make an assumption that CODE is a long integer - which again I may be wrong. Also my changes are prefaced by '***************
CREATE Precompiled Parameter Query
Create New Query called "qdfPrepMedian"
Copy/Paste SQL >> PARAMETERS [What Code] Long; SELECT Co, Base FROM <YourTableName> WHERE Code = [What Code] ORDER BY Base ASC;
Save the query
Adjusted Function
Option Explicit
'***********************
' changed posCode to Long
'***********************
Function FIncPercentile(ByVal posCode As Long, ByVal k As Single, ByVal tbl As String) As Variant
'***********************
' CREATE/USE Precompiled Parameter Query
' Create New Query called "qdfPrepMedian"
' Copy/Paste SQL >> PARAMETERS [What Code] Long; SELECT Co, Base FROM <YourTableName> WHERE Code = [What Code] ORDER BY Base ASC;
Const QRY_BY_CODES As String = "qdfPrepMedian"
Dim qdf As QueryDef
'
'***********************
Dim rstRec As Recordset
Dim db As Database
Dim n As Integer
Dim i As Single
' Declare all Currency variables on separate lines
' Otherwise they will be variants
Dim res As Currency
Dim d1 As Currency
Dim d2 As Currency
Set db = CurrentDb
'***********************
' Create readonly recordset from querydef
Set qdf = db.QueryDefs(QRY_BY_CODES)
qdf.Parameters("What Code") = posCode ' matches LONG variable passed to function
Set rstRec = qdf.OpenRecordset(dbOpenSnapshot, dbReadOnly) ' Readonly is sometimes faster
'***********************
' Use WITH rstRec
With rstRec
' Skip if there are no matches
If IsNull(!base) Or .RecordCount = 0 Then
'*** Is this a type ***
' FBasePercentile = Null
' Should it BE
FIncPercentile = Null
Exit Function
End If
' Count records
.MoveLast
n = .RecordCount
.MoveFirst
' Calculate the index where k is the percentile
i = n * k
' Test the decimal and find value accordingly
If i = Int(i) Then
.Move i - 1
d1 = !base
.MoveNext
d2 = !base
FIncPercentile = (d1 + d2) / 2
Else
i = Round(i + 0.5, 0)
.Move i - 1
FIncPercentile = !base
End If
End With
End Function