diff --git a/ClimsoftVer4/ClimsoftVer4/ClimsoftVer4.vbproj b/ClimsoftVer4/ClimsoftVer4/ClimsoftVer4.vbproj index 0ecdd45c..690e9bad 100644 --- a/ClimsoftVer4/ClimsoftVer4/ClimsoftVer4.vbproj +++ b/ClimsoftVer4/ClimsoftVer4/ClimsoftVer4.vbproj @@ -91,12 +91,16 @@ + + + + formAgro1.vb diff --git a/ClimsoftVer4/ClimsoftVer4/clsDataStructure.vb b/ClimsoftVer4/ClimsoftVer4/clsDataStructure.vb new file mode 100644 index 00000000..482dcfe2 --- /dev/null +++ b/ClimsoftVer4/ClimsoftVer4/clsDataStructure.vb @@ -0,0 +1,550 @@ +''' +''' +''' +''' The DataStructure will keep data in the same structure/table as in the database. +''' +Public Class DataStructure + ' TODO + ' 1. Do we allow choice of fields to get? If writing then all fields needed, + ' but reading only may not need all fields. + ' Most tables have few fields so might not be needed. + + ''' + ''' A string, the name of the table in the database this DataStructure links. + ''' + Private strTableName As String + + ''' + ''' A string, how the "id" column is stored in the database in all tables it appears in. + ''' This could be moved to a separate class of constants. + ''' + Protected strId As String = "id" + + ''' + ''' A string, how the "current"/"current best" column is stored in strTableName. + ''' This is "current_best" by default and expected to be either be "current" or "current_best". + ''' + Private strCurrent As String = "current_best" + + ''' + ''' A list of strings, the names of the fields in strTableName which uniquely define a row. + ''' In most tables and by default this is {strId, GlobalVariables.strVersionNumber} but it is not in all and can be changed. + ''' + Private lstKeyFieldNames As List(Of String) = New List(Of String)({strId, GlobalVariables.strVersionNumber}) + + ''' + ''' A list of strings, the names of the fields in strTableName which do not relate to primary key or auditting + ''' i.e. in most cases all columns apart from: strID, GlobalVariables.strVersionNumber and strCurrent. + ''' + ''' + ''' This is used when adding a new record. To add a new record, a value must be specified for each of lstValueFields. + ''' + Private lstValueFields As List(Of String) + + ' A TableFilter object which defines the rows in the table the values will be from + Private clsFilter As TableFilter + + ''' + ''' A DataTable storing the data read from the database. + ''' This remains static once fetched from the database. + ''' + Private dtbReadTable As DataTable + + ''' + ''' A DataTable storing the new data to be written to the database. + ''' This builds up as controls change. It is a clone of dtbReadTable + ''' with two extra columns: v_old and update_type + ''' + Private dtbUpdateTable As DataTable + + ''' + ''' A DataTable storing new comments associated to strTableName. + ''' + Private dtbComments As DataTable + + ''' + ''' A DataTable storing new events associated to strTableName. + ''' + Private dtbEvents As DataTable + + ''' + ''' A list of linked DataStructures that relate to required linked tables in the database. + ''' + ''' + ''' This will included DataStructures for existing comments and events associated to strTableName. + ''' + Private lstLinkedDataStructures As New List(Of DataStructure) + + ''' + ''' This event is raised when dtbReadTable changes. + ''' The navigator and page control will listen for this and update their values when this event is raised. + ''' + Public Event DataChanged() + + ''' Field name for "version old" when constructing dtbUpdateTable. + Private strVOld As String = ".v_old" + + ''' Field name for "update type" when constructing dtbUpdateTable. + Private strUpdateType As String = ".update_type" + + Public Sub New() + + End Sub + + Public Sub New(strNewTableName As String) + SetTableName(strTableName) + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' Set the name of the table in the database this DataStructure links to. + ''' + ''' A string, the name of the table in the database. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Public Sub SetTableName(strNewTableName As String) + strTableName = strNewTableName + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' + ''' Set the names of the fields in strTableName which uniquely define a row. This is + ''' needed when updating records. In most tables and by default this is {strId, + ''' strVersionNumber}. This method should only be used if it is different to the default. + ''' + ''' + ''' The list of names of the key fields for strTableName. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Public Sub SetKeyFields(iEnumerableNewKeyFields As IEnumerable(Of String)) + lstKeyFieldNames = iEnumerableNewKeyFields.ToList() + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' + ''' Set the names of the fields in strTableName which do not relate to primary key of auditting + ''' i.e. in most cases all columns apart from: strID, GlobalVariables.strVersionNumber and strCurrent. + ''' + ''' + ''' The list of names of the value fields for strTableName. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Public Sub SetValueFields(iEnumerableNewValueFields As IEnumerable(Of String)) + lstValueFields = iEnumerableNewValueFields.ToList() + End Sub + + ' Set the TableFilter + Public Sub SetFilter(clsNewFilter As TableFilter) + clsFilter = clsNewFilter + End Sub + + ' Alternative method to set the TableFilter + Public Sub SetFilter(strField As String, strOperator As String, strValue As String, Optional bIsPositiveCondition As Boolean = True, Optional bForceValuesAsString As Boolean = False) + Dim clsNewFilter As New TableFilter + + clsNewFilter.SetFieldCondition(strNewField:=strField, strNewOperator:=strOperator, objNewValue:=strValue, bNewIsPositiveCondition:=bIsPositiveCondition, bForceValuesAsString:=bForceValuesAsString) + SetFilter(clsNewFilter:=clsNewFilter) + End Sub + + ' Gets the data from database for strTableName and fills in dtbReadTable + ' bIncludedLinked - should linked DataStructure's also update their dtbReadTable? + Public Sub UpdateReadTable(Optional bIncludedLinked As Boolean = False) + 'TODO suspect this will be similar to DataCall.GetDataTable() + + If bIncludedLinked Then + For Each clsLinkedStruc As DataStructure In lstLinkedDataStructures + clsLinkedStruc.UpdateReadTable(bIncludedLinked) + Next + End If + End Sub + + ' Returns dtbReadTable + Public Function GetReadTable() As DataTable + Return dtbReadTable + End Function + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' + ''' Initialises dtbUpdateTable as a clone of dtbReadTable with two extra columns. + ''' This should be called after UpdateReadTable() has been called. + ''' + ''' + ''' .Clone copies the structure but not the data of a DataTable. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Private Sub InitialiseUpdateTable() + If dtbReadTable IsNot Nothing Then + dtbUpdateTable = dtbReadTable.Clone() + dtbUpdateTable.Columns.Add(strVOld, GetType(Integer)) + dtbUpdateTable.Columns.Add(strUpdateType, GetType(Integer)) + End If + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' + ''' Sets the comments associated with strTableName and stores them in dtbComments. + ''' This may be called when getting the main data, or may only be called on demand. + ''' + ''' + ''' (Optional) Boolean, if True all versions returned, if + ''' False only the current comment for each id is returned. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Private Sub SetCommentsTable(Optional bAllVersions As Boolean = False) + ' SQL command, all versions: + ' SELECT * FROM c5_comments WHERE table_id=@strTableName + + ' From here: https://stackoverflow.com/questions/612231/how-can-i-select-rows-with-maxcolumn-value-distinct-by-another-column-in-sql + ' SQL command, current only: + ' Select Case m.* From c5_comment m + ' INNER Join(SELECT id, MAX(current) As max_current FROM c5_comment GROUP BY id) m_group + ' ON m.id=m_group.id + ' AND m.current=m_group.max_current AND m.table_id=@strTableName + + ' alternative....... + + ' SELECT m.* + ' FROM c5_comment m + ' LEFT JOIN c5_comment b + ' ON m.id = b.id + ' AND m.current < b.current + ' WHERE b.current Is NULL AND m.table_id=@strTableName + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' + ''' Gets the events associated with strTableName and stores them in dtbEvents. This + ''' may be called when getting the main data, or may only be called on demand. + ''' + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Private Sub SetEventsTable() + ' SQL command, current only: + ' + ' SELECT * FROM (SELECT m.* + ' FROM c5_event_effect m + ' LEFT JOIN c5_event_effect b + ' ON m.id = b.id + ' AND m.current < b.current + ' WHERE b.current IS NULL AND m.table_id=@strTableName) e_e + ' INNER JOIN (SELECT m.* + ' FROM c5_event m + ' LEFT JOIN c5_event b + ' ON m.id = b.id + ' AND m.current < b.current + ' WHERE b.current IS NULL) e + ' ON e_e.event_id=e.id + End Sub + + ' Sets the lstLinkedDataStructures list + ' TODO Are there more useful ways to set this instead of passing in a list directly? + Public Sub SetLinkedDataStructures(lstNewListedDataStructures As List(Of DataStructure)) + + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' Creates a new action in the action table and returns its Action ID. + ''' + ''' Identifier for the action type. + ''' Identifier for the operator. + ''' + ''' The Action ID for the new action added. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Private Function NewAction(iActionTypeID As Integer, strOperatorID As String) As Integer + Dim iActionID As Integer + ' create new entry in action table using iActionTypeID, strOperatorID and current date-time + ' get back action ID for new entry created + ' This will be done manually initially: find max ActionId in the action table and increment. + Return iActionID + End Function + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' + ''' Updates dtbUpdateTable to add a new record to strTableName. + ''' This is only sent to the database when DoAction is called. + ''' + ''' + ''' A dictionary of values for the new record. The keys are the field names. + ''' This does not include the indentifiers and current fields. Their values are + ''' calculated automatically. + ''' + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Public Sub DoAddRecord(dctValues As Dictionary(Of String, Object)) + Dim lstValues As New List(Of Object) + Dim objTemp As Object = Nothing + + For Each strField As String In lstValueFields + If dctValues.TryGetValue(strField, objTemp) Then + lstValues.Add(objTemp) + Else + MsgBox("Developer error in clsDataStructure:DoAddRecord. No value specified for field: " & strField & " when attempting to add a record.") + Exit Sub + End If + Next + + ' This adds a row for the fields: + ' strId, strVersionNumber, lstValueFields(), strCurrent, strVOld, strUpdateType + ' The value for strId is DBNull.Value since this will be auto incremented in the database. + ' The order is important. + ' TODO: If this method will also be used for adding to the audit table then it will need to be adapted as it does not have the same id, version number, current structure + dtbUpdateTable.Rows.Add(DBNull.Value, 1, lstValueFields.ToArray, 1, DBNull.Value, GlobalVariables.UpdateType.NewRecord) + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' + ''' Adds rows to dtbUpdateTable to indicate a new and/or change to an event which + ''' causes records to change. + ''' This is only sent to the database when DoAction is called. + ''' + ''' A collection of DataRows for all event changes. These rows should + ''' have the same structure as dtbReadTable. + ''' This may be nothing if there are no event changes. + ''' + ''' A DataRow for a new event. This row should have the same structure + ''' as dtbReadTable. This may be nothing if there is no new event. + ''' + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Public Sub DoEvent(Optional rcEventChanges() As DataRow = Nothing, Optional rowEventNew As DataRow = Nothing) + Dim rowNew As DataRow + + ' Add the rows for the event changes. + If rcEventChanges IsNot Nothing Then + For Each rowCurrent As DataRow In rcEventChanges + rowNew = dtbUpdateTable.NewRow() + For i = 0 To rowCurrent.ItemArray.Count - 1 + rowNew.Item(i) = rowCurrent.ItemArray(i) + Next + rowNew.Item(strVOld) = DBNull.Value + rowNew.Item(strUpdateType) = GlobalVariables.UpdateType.EventChange + dtbUpdateTable.Rows.Add(rowNew) + Next + End If + + ' Add a row for the new event + If rowEventNew IsNot Nothing Then + rowNew = dtbUpdateTable.NewRow() + For i = 0 To rowEventNew.ItemArray.Count - 1 + rowNew.Item(i) = rowEventNew.ItemArray(i) + Next + rowNew.Item(strVOld) = DBNull.Value + rowNew.Item(strUpdateType) = GlobalVariables.UpdateType.EventNew + dtbUpdateTable.Rows.Add(rowNew) + End If + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' + ''' Adds two rows to dtbUpdateTable to indicate a correction to a record. + ''' One row is to modify the current of the existing row. The other is the new row for the + ''' correction. + ''' This is only sent to the database when DoAction is called. + ''' + ''' The DataRow to be corrected. + ''' A dictionary of values for the correction. The keys are the field names. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Public Sub DoCorrectionToRecord(rowCurrent As DataRow, dctValues As Dictionary(Of String, Object)) + Dim lstValues As New List(Of Object) + Dim objTemp As Object = Nothing + Dim rowCurrentUpdate As DataRow + Dim lstNewRecordItems As New List(Of Object) + + ' Add the row for the correction to the existing row. The only change is current set to NULL. + rowCurrentUpdate = dtbUpdateTable.NewRow() + For i = 0 To rowCurrent.ItemArray.Count - 1 + rowCurrentUpdate.Item(i) = rowCurrent.ItemArray(i) + Next + rowCurrentUpdate.Item(strCurrent) = DBNull.Value + rowCurrentUpdate.Item(strVOld) = rowCurrentUpdate.Item(GlobalVariables.strVersionNumber) + rowCurrentUpdate.Item(strUpdateType) = GlobalVariables.UpdateType.CorrectionOld + dtbUpdateTable.Rows.Add(rowCurrentUpdate) + + ' TODO Should be moved to separate function as duplicated elsewhere. + For Each strField As String In lstValueFields + If dctValues.TryGetValue(strField, objTemp) Then + lstValues.Add(objTemp) + Else + MsgBox("Developer error in clsDataStructure:DoAddRecord. No value specified for field: " & strField & " when attempting to add a record.") + Exit Sub + End If + Next + + ' This adds a row for the correction. + ' The fields are named: + ' strId, strVersionNumber, lstValueFields, strCurrent, strVOld, strUpdateType + ' strId is the same as in rowCurrent. + ' strVersionNumber is the existing version number + 1 + ' strCurrent is the existing current + ' strVOld is the existing version number + ' strUpdateType is Correction + ' The order is important. + lstNewRecordItems.Add(rowCurrent.Field(Of Integer)(strId)) + lstNewRecordItems.Add(rowCurrent.Field(Of Integer)(GlobalVariables.strVersionNumber) + 1) + lstNewRecordItems.AddRange(lstValues.ToArray) + lstNewRecordItems.Add(rowCurrent.Field(Of Integer)(strCurrent)) + lstNewRecordItems.Add(rowCurrent.Field(Of Integer)(GlobalVariables.strVersionNumber)) + lstNewRecordItems.Add(GlobalVariables.UpdateType.CorrectionNew) + dtbUpdateTable.Rows.Add(lstNewRecordItems.ToArray) + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' + ''' Adds a rows to dtbUpdateTable to indicate a deleted record. + ''' This is only sent to the database when DoAction is called. + ''' + ''' The DataRow to be deleted. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Public Sub DoDeleteRecord(rowDelete As DataRow) + Dim lstValues As New List(Of Object) + Dim rowDeleteUpdate As DataRow + + ' Add the row for the deletion. + rowDeleteUpdate = dtbUpdateTable.NewRow() + For i = 0 To rowDelete.ItemArray.Count - 1 + rowDeleteUpdate.Item(i) = rowDelete.ItemArray(i) + Next + rowDeleteUpdate.Item(strCurrent) = DBNull.Value + rowDeleteUpdate.Item(strVOld) = rowDeleteUpdate.Item(GlobalVariables.strVersionNumber) + rowDeleteUpdate.Item(strUpdateType) = GlobalVariables.UpdateType.Delete + dtbUpdateTable.Rows.Add(rowDeleteUpdate) + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' Executes the action operation. + ''' + ''' This could be moved to a separate class? + ''' + ''' The ID of the action type by run. + ''' The ID of the operator performing the action. + ''' The DataStructure containing changes that form the action. + ''' (Optional) An comment on the action. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Public Sub DoAction(iActionTypeID As Integer, strOperatorID As String, DataStruct As DataStructure, Optional strActionComment As String = "") + Dim iActionID As Integer + + ' create new entry in action table using iActionTypeID, strOperatorID and current date-time + ' get back action ID for new entry created + iActionID = NewAction(iActionTypeID, strOperatorID) + ' TODO Create comment for action comment and add to comment table + + ' Sends changes from the update table to the database for DataStructures + DataStruct.UpdateTable(iActionTypeID) + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' Performs the updates to the tables in this DataStructure associated with + ''' the action with id iActionID. This including writing the audit, comments, + ''' and events. + ''' + ''' + ''' The ID of the action being performed. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Public Sub UpdateTable(iActionID As Integer) + Dim dtbAuditTable As DataTable + Dim iUpdateType As Integer + Dim dtbWriteTable As DataTable + + ' dtbWriteTable will write changes to the database. A copy of dtbUpdateTable with last two columns removed. + dtbWriteTable = Me.dtbUpdateTable.Copy() + dtbWriteTable.Columns.Remove(strVOld) + dtbWriteTable.Columns.Remove(strUpdateType) + + ' Make a blank audit table to add entries to + dtbAuditTable = GlobalVariables.dtbEmptyAuditTable + + ' Construct the audit table + For i As Integer = 0 To Me.dtbUpdateTable.Rows.Count - 1 + iUpdateType = dtbUpdateTable.Rows(i).Field(Of Integer)(strUpdateType) + Select Case iUpdateType + Case GlobalVariables.UpdateType.NewRecord + 'TODO Need to get the entry "id" of the new record to be added - replace 9999 + GlobalVariables.AddToAuditTable(dtbAuditTable, iActionID, strTableName, 9999, Nothing, 1) + Case GlobalVariables.UpdateType.CorrectionOld + ' No audit entry to add + Case GlobalVariables.UpdateType.CorrectionNew + GlobalVariables.AddToAuditTable(dtbAuditTable, iActionID, strTableName, dtbUpdateTable.Rows(i).Field(Of Integer)(strId), dtbUpdateTable.Rows(i).Field(Of Integer)(strVOld), dtbUpdateTable.Rows(i).Field(Of Integer)(GlobalVariables.strVersionNumber)) + Case GlobalVariables.UpdateType.EventChange + ' No audit entry to add + Case GlobalVariables.UpdateType.EventNew + GlobalVariables.AddToAuditTable(dtbAuditTable, iActionID, strTableName, dtbUpdateTable.Rows(i).Field(Of Integer)(strId), Nothing, dtbUpdateTable.Rows(i).Field(Of Integer)(GlobalVariables.strVersionNumber)) + Case GlobalVariables.UpdateType.Delete + GlobalVariables.AddToAuditTable(dtbAuditTable, iActionID, strTableName, dtbUpdateTable.Rows(i).Field(Of Integer)(strId), dtbUpdateTable.Rows(i).Field(Of Integer)(GlobalVariables.strVersionNumber), Nothing) + End Select + Next + + ' Write: update table, audit table, comments table, events table + ' TODO call methods to write to the database using the data adapter + + ' Call UpdateTable for each lstLinkedDataStructures + For Each clsLinkedStruc As DataStructure In lstLinkedDataStructures + clsLinkedStruc.UpdateTable(iActionID) + Next + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' Test of updating values. + ''' + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Public Sub TestUpdateTable() + Dim dctCorrectionValues As New Dictionary(Of String, Object) + Dim dtbExpectedUpdateTable As DataTable + Dim dtbTempReadTable As DataTable + Dim rowTempEvent1 As DataRow + Dim rowTempEvent2 As DataRow + Dim rcTempEventChanges() As DataRow + + dtbReadTable = New DataTable + dtbReadTable.Columns.Add(strId, GetType(Integer)) + dtbReadTable.Columns.Add(GlobalVariables.strVersionNumber, GetType(Integer)) + dtbReadTable.Columns.Add("value", GetType(String)) + dtbReadTable.Columns.Add(strCurrent, GetType(Integer)) + + ' Temporary read table used to create new rows that are passed in to Do methods + dtbTempReadTable = dtbReadTable.Clone() + + dtbReadTable.Rows.Add(1, 1, "a", 1) + dtbReadTable.Rows.Add(2, 1, "b", 1) + dtbReadTable.Rows.Add(3, 1, "c", 1) + dtbReadTable.Rows.Add(4, 1, "dd", 1) + dtbReadTable.Rows.Add(4, 2, "ddd", 2) + dtbReadTable.Rows.Add(5, 1, "e", 1) + + InitialiseUpdateTable() + + SetValueFields({"value"}) + + '' CHANGES + ' Correction in id=1 from "a" to "A" + dctCorrectionValues.Add("value", "A") + DoCorrectionToRecord(dtbReadTable.Rows(0), dctCorrectionValues) + + ' Event change in id=2 "b" to "bb" + ' New event added after existing event - no change to existing event + dtbTempReadTable.ImportRow(dtbReadTable.Rows(1)) + rowTempEvent1 = dtbTempReadTable.Rows(dtbTempReadTable.Rows.Count - 1) + rowTempEvent1.Item("value") = "bb" + rowTempEvent1.Item(GlobalVariables.strVersionNumber) = rowTempEvent1.Field(Of Integer)(GlobalVariables.strVersionNumber) + 1 + rowTempEvent1.Item(strCurrent) = rowTempEvent1.Field(Of Integer)(strCurrent) + 1 + DoEvent(rowEventNew:=rowTempEvent1) + + ' Delete in id=3 + DoDeleteRecord(dtbReadTable.Rows(2)) + + ' Event change in id=4 + ' New event added before existing two events, changes required to both existing events + dtbTempReadTable.ImportRow(dtbReadTable.Rows(3)) + rowTempEvent2 = dtbTempReadTable.Rows(dtbTempReadTable.Rows.Count - 1) + rowTempEvent2.Item("value") = "d" + rowTempEvent2.Item(GlobalVariables.strVersionNumber) = 3 + rowTempEvent2.Item(strCurrent) = 1 + rcTempEventChanges = dtbReadTable.Select("id = 4") + rcTempEventChanges(0).Item(strCurrent) = 2 + rcTempEventChanges(1).Item(strCurrent) = 3 + DoEvent(rcTempEventChanges, rowTempEvent2) + + dtbExpectedUpdateTable = dtbReadTable.Clone() + dtbExpectedUpdateTable.Columns.Add(strVOld, GetType(Integer)) + dtbExpectedUpdateTable.Columns.Add(strUpdateType, GetType(Integer)) + dtbExpectedUpdateTable.Rows.Add(1, 1, "a", DBNull.Value, 1, GlobalVariables.UpdateType.CorrectionOld) + dtbExpectedUpdateTable.Rows.Add(1, 2, "A", 1, 1, GlobalVariables.UpdateType.CorrectionNew) + dtbExpectedUpdateTable.Rows.Add(2, 2, "bb", 2, DBNull.Value, GlobalVariables.UpdateType.EventNew) + dtbExpectedUpdateTable.Rows.Add(3, 1, "c", DBNull.Value, 1, GlobalVariables.UpdateType.Delete) + dtbExpectedUpdateTable.Rows.Add(4, 3, "d", 1, DBNull.Value, GlobalVariables.UpdateType.EventNew) + dtbExpectedUpdateTable.Rows.Add(4, 1, "dd", 2, DBNull.Value, GlobalVariables.UpdateType.EventChange) + dtbExpectedUpdateTable.Rows.Add(4, 2, "ddd", 3, DBNull.Value, GlobalVariables.UpdateType.EventChange) + + UpdateTable(1) + End Sub +End Class diff --git a/ClimsoftVer4/ClimsoftVer4/clsDataStructureStation.vb b/ClimsoftVer4/ClimsoftVer4/clsDataStructureStation.vb new file mode 100644 index 00000000..7317614a --- /dev/null +++ b/ClimsoftVer4/ClimsoftVer4/clsDataStructureStation.vb @@ -0,0 +1,109 @@ +Public Class DataStructureStation + Inherits DataStructure + + ' Will be able to view/get station data as a single table + + ' Properties of station to get: all station properties and all features (all features relate to stations) + + + ' Top level table: feature_type + ' Linked tables: + ' 1. feature (get list of stations) + ' 1a. feature_geometry (get list of latitude and longitude for all stations) + ' 2. feature_type_property (get list of properties defined for stations) + ' 2a. feature_property_value (get list of property values for all properties for all stations) + + ' Everything that is fixed for this station class + ' e.g. the top level table name and the list of sub DataStructures + + ' Define these here so they can be accessible in multiple functions + Private clsFeature As New DataStructure("c5_feature") + Private clsFeatureGeometry As New DataStructure("c5_feature_geometry") + Private clsFeatureTypeProperty As New DataStructure("c5_feature_type_property") + Private clsFeaturePropertyValue As New DataStructure("c5_feature_property_value") + + ' This is the ID of the station entry in the feature_type table + ' This needs to be retrieved from the database + ' This is needed to get linked data from all other feature tables + Private iFeatureTypeID As Integer + + Private strFeatureTypeIdField As String = "feature_type_id" + + Public Sub New() + SetTableName("c5_feature_type") + End Sub + + ' Gets the ID of the station entry in the feature_type table and sets to iFeatureTypeID + ' This only needs to be called once per session + Private Sub SetFeatureTypeID() + Dim iStationID As Integer + + ' TODO get this from the database and set to iFeatureTypeID + ' How do we get this from the database? Do we assume that the name field will be "station"? What about in other languages? + ' Or can we assume it is a fixed ID across databases? + iFeatureTypeID = iStationID + End Sub + + ' Linked tables: + ' 1. feature (get list of stations) + ' 1a. feature_geometry (get list of latitude and longitude for all stations) + ' 2. feature_type_property (get list of properties defined for stations) + ' 2a. feature_property_value (get list of property values for all properties for all stations) + + ' This should be called after calling SetFeatureTypeID() + Private Sub SetStationLinkedDataStructures() + clsFeature.SetFilter(strFeatureTypeIdField, "=", iFeatureTypeID) + clsFeatureTypeProperty.SetFilter(strFeatureTypeIdField, "=", iFeatureTypeID) + + clsFeature.SetLinkedDataStructures(New List(Of DataStructure)(clsFeatureGeometry)) + clsFeatureTypeProperty.SetLinkedDataStructures(New List(Of DataStructure)(clsFeaturePropertyValue)) + SetLinkedDataStructures(New List(Of DataStructure)({clsFeature, clsFeatureTypeProperty})) + End Sub + + ' strPropertyName is a string/enumerated value of known station properties + ' Method knows how to get a know property in v4 or v5 database + ' if strPropertyName is not recognised then have generic way to search database + Public Function GetPropertyValue(strPropertyName As String) As Object + + End Function + + ' Returns all station related data in a single table + Public Function GetStationDataTable() As DataTable + Dim dtbStation As DataTable + + UpdateReadTable(True) + + ' Create a clone of the feature table as a base for a "station" table + dtbStation = clsFeature.GetReadTable().Clone() + ' Left join feature with feature_geometry table to get latitude and longitude columns + ' Select a, b specifies what is returned in the query, a and b ensures all fields available + Dim query = From a In clsFeature.GetReadTable().Copy().AsEnumerable() + Group Join b In clsFeatureGeometry.GetReadTable.Copy() + On a.Field(Of Integer)(strId) Equals b.Field(Of Integer)("feature_id") + Into Group + Let b = Group.FirstOrDefault + Select a, b + + ' TODO how do we deal with different versions when merging tables? + ' TODO which fields do we keep from both tables? + For Each item In query + dtbStation.Rows.Add(item.a.ItemArray) + dtbStation.Rows(dtbStation.Rows.Count - 1)("latitude") = item.b.Item("latitude") + dtbStation.Rows(dtbStation.Rows.Count - 1)("longitude") = item.b.Item("longitude") + Next + + ' Next: + ' 1. Merge dtbFeaturePropertyValue with dtbFeatureTypeProperty + ' (right join if possible to only get properties for station, otherwise filter manually) + ' 2. Unstack result (separate unstack for each type: integer, string etc.) to get properties wide + ' 3. Merge unstacked results together + ' 4. Merge result with dtbStation + + End Function + + ' Next: + ' Add set of Do* functions to manage changes in the DataStructures + ' e.g. DoCorrectPropertyValue(), DoAddPropertyValue(), DoAddStation() + ' Forms can call these by only needing access to the output from GetStationDataTable() + ' The Do* functions then call the appropriate Do* base DataStructure function for the right table +End Class \ No newline at end of file diff --git a/ClimsoftVer4/ClimsoftVer4/clsGlobalVariables.vb b/ClimsoftVer4/ClimsoftVer4/clsGlobalVariables.vb new file mode 100644 index 00000000..17c1ce64 --- /dev/null +++ b/ClimsoftVer4/ClimsoftVer4/clsGlobalVariables.vb @@ -0,0 +1,134 @@ +'--------------------------------------------------------------------------------------------------- +' file: clsGlobalVariables.vb +' +' summary: GlobalVariables class +'--------------------------------------------------------------------------------------------------- + +'''//////////////////////////////////////////////////////////////////////////////////////////////////// +''' Global Variables. +'''//////////////////////////////////////////////////////////////////////////////////////////////////// + +Public Class GlobalVariables + + ''' + ''' A string, how the "version number" column is stored in the database in all tables it appears in. + ''' + Public Shared ReadOnly strVersionNumber As String = "version_number" + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' Values that represent update types. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + + Public Enum UpdateType + ''' An enum constant representing the new record option. When a new record is added. + NewRecord + ''' An enum constant representing the correction old option. When a correction is made to an existing record, this is the existing version. + CorrectionOld + ''' An enum constant representing the correction option. When a correction is made to an existing record, this is the new version. + CorrectionNew + ''' An enum constant representing the event change option. When a change is made to an event that is not a correction e.g. another event is added before this resulting in a change to its current. + EventChange + ''' An enum constant representing the event new option. When a new value is added to an existing record corresponding to a new event. + EventNew + ''' An enum constant representing the delete option. When a record is deleted. + Delete + End Enum + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' Gets the dtb empty audit table. + ''' + ''' The dtb empty audit table. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + + Public Shared ReadOnly Property dtbEmptyAuditTable As DataTable + Get + Dim dtbAudit As New DataTable + + dtbAudit.Columns.Add("id", GetType(Integer)) + dtbAudit.Columns.Add("action_id", GetType(Integer)) + dtbAudit.Columns.Add("table", GetType(String)) + dtbAudit.Columns.Add("entry_id", GetType(Integer)) + dtbAudit.Columns.Add("version_old", GetType(Integer)) + dtbAudit.Columns.Add("version_new", GetType(Integer)) + Return dtbAudit + End Get + End Property + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' Adds to audit table. + ''' + ''' The dtb audit. + ''' The table. + ''' Identifier for the entry. + ''' Zero-based index of the version old. + ''' Zero-based index of the version new. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Public Shared Sub AddToAuditTable(dtbAudit As DataTable, iActionID As Integer, strTable As String, iEntryId As Integer, iVersionOld As Nullable(Of Integer), iVersionNew As Nullable(Of Integer)) + Dim dtbAuditSchema As DataTable + Dim objParams As New List(Of Object) + + dtbAuditSchema = dtbEmptyAuditTable + If dtbAudit.Columns.Count <> dtbAuditSchema.Columns.Count Then + MsgBox("Developer error in GlobalVariables.AddToAuditTable: Cannot add records to audit table. Audit table provided does not match audit table schema (incorrect number of columns).", MsgBoxStyle.Information, Title:="Developer error") + Exit Sub + End If + For i As Integer = 0 To dtbAudit.Columns.Count - 1 + If dtbAudit.Columns(i).ColumnName <> dtbAuditSchema.Columns(i).ColumnName OrElse dtbAudit.Columns(i).DataType <> dtbAuditSchema.Columns(i).DataType Then + MsgBox("Developer error in GlobalVariables.AddToAuditTable: Cannot add records to audit table. Audit table provided does not match audit table schema (incorrect column name/type).", MsgBoxStyle.Information, Title:="Developer error") + Exit Sub + End If + Next + ' id can be blank since it is unique and can be set to auto increment in the database + objParams.Add(DBNull.Value) + objParams.Add(iActionId) + objParams.Add(strTable) + objParams.Add(iEntryId) + objParams.Add(If(iVersionOld Is Nothing, DBNull.Value, iVersionOld)) + objParams.Add(If(iVersionNew Is Nothing, DBNull.Value, iVersionNew)) + dtbAudit.Rows.Add(objParams.ToArray) + End Sub + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' + ''' Constructs an SQL query to get only the current versions from a table. + ''' Could be moved to a separate class for query construction + ''' + ''' + ''' A string, the name of the table to query. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Private Function GetCurrentVersionsQuery(strTable As String) As String + ' From here: https://stackoverflow.com/questions/612231/how-can-i-select-rows-with-maxcolumn-value-distinct-by-another-column-in-sql + + ' Option 1: + ' ######### + ' Select Case m.* From @strTable m + ' INNER Join(SELECT id, MAX(@strCurrent) As max_current FROM @strTable GROUP BY id) m_group + ' ON m.id=m_group.id + ' AND m.@strCurrent=m_group.max_current + + ' Option 2: + ' ######### + ' SELECT m.* + ' FROM @strTable m + ' LEFT JOIN @strTable b + ' ON m.id = b.id + ' AND m.@strCurrent < b.@strCurrent + ' WHERE b.@strCurrent Is NULL + End Function + + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + ''' + ''' Constructs an SQL query to get only the versions equal to iVersion from a table. + ''' (Could be moved to a separate class for query construction) + ''' + ''' + ''' A string, the name of the table to query. + ''' An integer, the version numbers to extract for each id. + ''' + ''' An SQL query string. + '''//////////////////////////////////////////////////////////////////////////////////////////////////// + Private Function GetVersionNumberQuery(strTable As String, iVersion As Integer) As String + + End Function + +End Class \ No newline at end of file diff --git a/ClimsoftVer4/ClimsoftVer4/clsTests.vb b/ClimsoftVer4/ClimsoftVer4/clsTests.vb new file mode 100644 index 00000000..bc1a234e --- /dev/null +++ b/ClimsoftVer4/ClimsoftVer4/clsTests.vb @@ -0,0 +1,56 @@ +Public Class clsTests + + Public Sub TestStationTable() + Dim dtbFeature As New DataTable + Dim dtbFeatureGeometry As New DataTable + Dim dtbFeatureTypeProperty As New DataTable + Dim dtbFeaturePropertyValue As New DataTable + Dim dtbStation As New DataTable + + dtbFeature.Columns.Add("id", GetType(Integer)) + dtbFeature.Columns.Add("version_number", GetType(Integer)) + dtbFeature.Columns.Add("feature_type_id", GetType(Integer)) + dtbFeature.Columns.Add("name", GetType(String)) + dtbFeature.Columns.Add("current", GetType(Integer)) + + dtbFeature.Rows.Add(1, 1, 1, "BINGA", 1) + dtbFeature.Rows.Add(2, 1, 1, "KARIBA AIRPORT", 1) + 'dtbFeature.Rows.Add(3, 1, 2, "Kenya", 1) + dtbFeature.Rows.Add(4, 1, 1, "Kisumu Airport", 1) + + dtbFeatureGeometry.Columns.Add("id", GetType(Integer)) + dtbFeatureGeometry.Columns.Add("version_number", GetType(Integer)) + dtbFeatureGeometry.Columns.Add("feature_id", GetType(Integer)) + dtbFeatureGeometry.Columns.Add("geometry_type", GetType(String)) + dtbFeatureGeometry.Columns.Add("shape", GetType(String)) + dtbFeatureGeometry.Columns.Add("latitude", GetType(Double)) + dtbFeatureGeometry.Columns.Add("longitude", GetType(Double)) + dtbFeatureGeometry.Columns.Add("current", GetType(Integer)) + + dtbFeatureGeometry.Rows.Add(1, 1, 1, "point", "", -1.6, 37.3, 1) + dtbFeatureGeometry.Rows.Add(2, 1, 2, "point", "", -16.5, 28.8, 1) + 'dtbFeatureGeometry.Rows.Add(3, 1, 3, "polygon", "", DBNull.Value, DBNull.Value, 1) + dtbFeatureGeometry.Rows.Add(4, 1, 4, "point", "", -0.01, 34.7, 1) + + dtbStation.Columns.Add("id", GetType(Integer)) + dtbStation.Columns.Add("version_number", GetType(Integer)) + dtbStation.Columns.Add("feature_type_id", GetType(Integer)) + dtbStation.Columns.Add("name", GetType(String)) + dtbStation.Columns.Add("current", GetType(Integer)) + dtbStation.Columns.Add("latitude", GetType(Double)) + dtbStation.Columns.Add("longitude", GetType(Double)) + + Dim query = From a In dtbFeature.AsEnumerable + Group Join b In dtbFeatureGeometry + On a.Field(Of Integer)("id") Equals b.Field(Of Integer)("feature_id") + Into Group + Let b = Group.FirstOrDefault + Select a, b + For Each item In query + dtbStation.Rows.Add(item.a.ItemArray) + dtbStation.Rows(dtbStation.Rows.Count - 1)("latitude") = item.b.Item("latitude") + dtbStation.Rows(dtbStation.Rows.Count - 1)("longitude") = item.b.Item("longitude") + Next + + End Sub +End Class