Een lege datum invullen in de databank: NOT NULL?

Bij het updaten van een record krijg ik een eigenaardige foutmelding dat ik een verkeerde waarde “11” probeer in te vullen in een datumveld “dateout”.

Een ander veld met datum werkt wel goed, maar dat wordt dan ook wel van gegevens voorzien, namelijk de huidige datum van de update.

Public Sub updateProduct(id As Long, longname As String, shortname As String, ourname As String, purpose As String, remark As String, sup_id As Integer, sup_nick As String, sup_qty As Float, sup_qtypack As String, datein As Date, dateout As Date, sdsdate As Date, sdsfile As String, sdsok As Integer, active As Integer) As Boolean 

  Dim sSql As String 
  Dim myResult As Result 

  sSql = "UPDATE product SET "
  sSql &= "name=&2"
  ...
  sSql &= ", sup_qty=&9"
  sSql &= ", sup_qtypack=&10"
  sSql &= ", dateout=&11"
  ...
  sSql &= ", active=&16"
  sSql &= ", updat=NOW()"
  sSql &= ", updby=&17"
  sSql &= " WHERE id =&1 " 

If goConnect()
  Try $hconData.Exec(sSql, id, name, ..., sup_qty, sup_qtypack, dateout, ...., active, System.User.Name)
Else
  ...
Endif

Het enige wat ik met wat opzoekwerk kan vaststellen is dat die 11 niet de waarde is die ik stuur naar de databank, maar wel het volgnummer van de parameter in de parameterlijst die naar het commando wordt gegeven; zoals in:

hConData.Exec(sSql, par1, par2, par3, par4, par5, par6, par7, par8, par9, par10, par11, par12)

Ik stuur bij de update inderdaad alle velden naar de mysql-verwerker, zodat ik maar 1 update procedure moet maken.

Voor tekstvelden is het niet erg om een lege string “” te sturen, dat werkt.
Maar bij een datum? Wat moet je daar sturen? Null? NULL? “NULL”?
Na een paar pogingen werkt het terug als ik de datumvelden niet stuur met parameters, maar vervang door de tekst veld=NULL, als in:

  Dim sSql As String 
  Dim myResult As Result 

  sSql = "UPDATE product SET "
  sSql &= "name=&2"
  ...
  sSql &= ", sup_qty=&9"
  sSql &= ", sup_qtypack=&10"
  
  If dateout = Null
    sSql &= ", dateout=NULL"
  Else
    sSql &= ", dateout=&11"
  Endif
  ...
  sSql &= ", active=&16"
  sSql &= ", updat=NOW()"
  sSql &= ", updby=&17"
  sSql &= " WHERE id =&1 " 

If goConnect() 
    Try $hconData.Exec(sSql, id, name, ..., sup_qty, sup_qtypack, dateout, ...., active, System.User.Name)
Else
  ...
Endif

Lettergrootte (font size)

Kleine worsteling met de lettergrootte in Gambas3 applicatie: in principe kan je in code de lettergrootte vergroten/verkleinen met

inc object.font.size
dec object.font.size

Toegepast in FMain():

DEC ME.Font.Size
… heeft als gevolg dat de letters van het menu kleiner worden, de rest van het aktieve venster niet!
Hetzelfde daar met:
DEC Application.Font.Size

In FMain heb ik een splitview HSplit1, waarbinnen al mijn andere schermelementen zitten.
Pas ik de font-aanpassing toe op die split, dan veranderen alle “onderliggende” fonts mee:

Inc HSplit1.Font.Size
Dec HSplit1.Font.Size

Voor mij is dit prima. Het menu verandert niet mee, maar dan blijft het zeker leesbaar.
(ik verander de fontgrootte vanuit een menu-item).

Public Sub mniFontInc_Click()
  Inc HSplit1.Font.Size
End

TableView edit: sommige velden wijzigen, andere cel enkel leesbaar

De volgende stap bij het maken van een tabel met gegevens (TableView) is bepalen welke cel wel of niet mag gewijzigd worden. In dit artikel Hoe data in een cel steken van een TableView werd de tabel gecreëerd, en werd bij een klik de cel wijzigbaar met “.Edit”.

Vanaf je op een cel klikt, is die op het scherm wijzigbaar. De gegevens echt bewaren moet je verder zelf doen, maar misschien wil je al beperken welke cellen gewijzigd kunnen worden. Voorbeeld is een tabel die een record uit de databank weergeeft:

Kolom recordnummer, kolom veldnaam, kolom waarde in het veld. Die laatse willen we wijzigen.
Ik verkies een extra kolom voor de nieuwe waarde, waarbij de gebruiker nog kan zien wat er voordien stond.
Kolomtitels: “fieldname”, “current value”, “new value”
Kolom index 0, 1, 2; kolom 2 moet wijzigbaar zijn. (definieer bovenaan een constante voor deze kolom)

Public Const iColNewVal As Integer = 2

De rijen zijn de velden: rij 1 is id, tweede, derde enz zijn de andere velden van je record.
Je wil meestal niet dat het veld van de id kan gewijzigd worden.
Achteraan zit misschien nog een veld met de datum van aanmaak van het record, die moet ook hetzelfde blijven.

We maken een array om alle kolommen die wijzigbaar zijn in bij te houden:
Private arrEditMask As New Integer[]
Als Array.Find (van een rijwaarde) als resultaat -1 geeft komt ze niet voor.

Zet elke kolom erin die je nodig hebt:
arrEditMask.Add(i)

We reageren in de tabel op een klik als het een “wijzigbaar” veld is:

Public Sub TableView1_Click()
  
  Debug "Is current row ? = " & TableView1.Row
  Debug " is current column ? = " & TableView1.Column
  
  If TableView1.Column = iColNewVal
    If arrEditMask.Find(TableView1.Row) > -1
      TableView1.Edit()
    Endif
  Endif

End

En om de waarde te bewaren in de tabel (zichtbaar te maken na verlaten van het veld) moet je:

Public Sub TableView1_Save(x As Integer, y As Integer, sText As String)

      TableView1[x, y].Text = sText

End

Om de waarde te bewaren in het record in de databank moet je zelf verder uitwerken.

Wachtwoord onleesbaar opslaan (versleutelen, ontsleutelen)

Wachtwoord-check

Crypt

Er bestaat een Gambas bibliotheek (component) voor wachtwoord versleuteling en checken van een wachtwoord:

Crypt

De Crypt klasse levert : Check DES MD5 SHA256 SHA512

Hiermee kan je een wachtwoord versleutelen met DES of MD5, algoritmes uit de GNU libc bibliotheek.

ps: Zoals in Linux gebruikt, op de commandolijn met md5sum abc.txt om de checksum van (download) bestand abc.txt te krijgen.

Je kan ook de checksum van een gegeven wachtwoord vergelijken met de opgeslagen checksum van een eerder gegeven wachtwoord (waarbij je dat origineel wachtwoord niet opslaat, maar enkel de checksum); als die klopt is het gegeven wachtwoord juist.

Versleutel-ontsleutel

OpenSSL

Er is ook een bibliotheek die toelaat tekst te versleutelen en ontsleutelen (IDE-menu Project, Properties, Components): gb.openssl
OpenSSL levert:

  • Cipher de cipher (block en stream) algoritmes van OpenSSL. Wikipedia: Ciphers.
  • CipherText
  • Digest de hash algoritmes van het OpenSSL project. Wikipedia: Cryptographic hash function
  • HMac op hash gebaseerde berichten-toegangscodering (HMAC). Wikipedia: HMAC

Algoritmes

Er bestaan verschillende versleutelalgoritmes (en “operation modes”), en je kan die na het kiezen van de openssl library ook opvragen.
Maak een nieuw leeg project in Gambas3 (misschien werkt het ook in 2), en in de MMain vul je onderstaande code in.
Die toont op de commandolijn alle aanwezige algoritmes, en bewaart ze ook in een bestand (~/.config/gambas3/CipherList.conf)

CipherList

Private sName As String
Private icount As Integer

Public Sub Main()
  Settings["Cipher/date"] = Str$(Now())
  For Each sname In Cipher.List.Sort()
    Print sName; "\n";
    Settings["Cipher/" & Str$(icount)] = sName
    Inc icount
  Next
  
End

Je krijgt een lijst van bv 89 mogelijkheden (zie lijst onderaan); ze bestaan uit de encryptiemethode (plus bit-sterkte) en een “mode”, bv CBC staat voor Cipher Block Chaining (zie hieronder “operation mode” *).

Enkele algoritmen uit de lijst:

  • AES-128-CBC staat eerste in de lijst, gebruikt in dit vb.
  • BlowFish (Schneier) is veelgebruikt, maar ondertussen door hem als verouderd beschouwd (zie twofish).
  • DES=Data Encryption Standard: werd bekend om zijn korte sleutel, gedeeltelijke geheimhouding en betrokkenheid van NSA.
  • AES=Advanced Encryption Standard: Rijndael (ontwikkeld door twee Belgen: V. Rijmen & J. Daemen): opvolger van DES (Amerikaanse standaard). AES-128 wordt nog steeds als veilig gezien (2006).
  • , en spijtig genoeg niet in de lijst: twofish.

Je hebt de letterlijke tekstwaarde van de gekozen methode nodig om het algoritme op te roepen in je programma.

* Operation mode
Bepaalt op welke manier het oorspronkelijk te versleutelen bericht benaderd wordt voor versleuteling.
De “mode” wordt aangegeven door de extra lettercombinatie achteraan:

  • ECB=Electronic CodeBook: bericht wordt in stukjes gekapt, elk stuk versleuteld. Zwakte: patronen worden zichtbaar! (vgl elke letter apart versleutelen; patroon van de “e” wordt zichtbaar)
  • * CBC=Cipher Block Chaining: er wordt op iedere blok een bewerking gedaan tov het vorige blok, en een initialisatie vector moet gebruikt worden voor het eerste blok. De-cryptie kan parrallel, en Random Read Acces is mogelijk.
  • * CTR=CounTeR of ICM=Integer Counter Mode of SIC=Segmented Integer Counter: maakt van een block cipher een stream cipher. En+De-cryptie kan parrallel, en Random Read Acces is mogelijk.
  • PCBC=Propagating Cipher Block Chaining: gebruikt ook initialisatievector.
  • CFB=Cipher Feedback

* aanbevolen methode door o.a. Bruce Schneier (van Schneier on Security)

Meer info zie wikipedia Block_cipher_mode_of_operation

Tekst versleutelen – ontsleutelen

Het volgende stukje programma kan je toevoegen aan het vorige, het geeft een encryptie en decryptie van “Geheim”:

  Print Cipher["AES-128-CBC"].Encrypt("Geheim")
  Print Cipher["AES-128-CBC"].Decrypt(Cipher["AES-128-CBC"].Encrypt("Geheim"))

Het resultaat:

(CipherText 0x1ce7cf8)
Geheim

Meestal zal je de encrypt en decrypt niet in één regel gebruiken, en gebruik je een variabele, en dat is hier geen eenvoudige tekst (string).
Om die te declareren heb je de speciale variabele uit Cipher nodig:

Private hEncrypted As CipherText

En de code met encrypt naar variabele, en apart decrypt:

  hEncrypted = Cipher["AES-128-CBC"].Encrypt("Geheim")
  Print hEncrypted
  Print Cipher["AES-128-CBC"].Decrypt(hEncrypted)

De encryptie-methode steek je in een constante, bv PRIVATE Const sEncryptmethod AS String = “AES-128-CBC”.
In de code hier net boven ontbreken trouwens nog de Key en de InitVector.

Versleutelde tekst in bestand

In het geval dat de versleutelde tekst in een bestand zit, en daarnaar geschreven + daaruit gelezen moet worden, kan het hetvolgende programma gebruiken:

' Gambas module file

Private hEncrypted As CipherText
Private sLine As String
Private sCryptedFile As String
Private Const sAlgorithm As String = "AES-128-CBC"
Private sKey As String ' 32 ??
Private sInitVector As String ' 16


Public Sub Main()

  Print "Hello cipher world 2" ' 
  sCryptedFile = User.Home &/ "cipher.bin"
  hEncrypted = Null
  ' can be asked from user:
  
  sKey = "123456789012345678901234567890123"
  sInitVector = "1234567890123ABC"

  Print "Geef een woord of tekst (einde met enter):"
  Line Input sLine

  Try hEncrypted = Cipher[sAlgorithm].Encrypt(sLine, sKey, sInitVector)
  If Error
    Print Error.Text & " with key " & sKey
    TestKeyLength() 
  Endif

  File.Save(sCryptedFile, hEncrypted.Cipher)

  Print Base64$(File.Load(sCryptedFile))
  
  sLine = ""
  Print "I Decrypt " & sCryptedFile & " as : " 
  ' sLine = Cipher["AES-128-CBC"].Decrypt(File.Load(sCryptedFile))
  ' TYPE Mismatch, wanted CipherText, got String instead
  ' split up:
  hEncrypted = New CipherText(File.Load(sCryptedFile), sKey, sInitVector)
  ' now it seems we have to use the key and initvector, even if not required to encrypt ...
  
  sLine = Cipher[sAlgorithm].Decrypt(hEncrypted)
  Print sLine 
  
End

Print Base64$ maakt het een beetje leesbaar op de console zonder dat er rare dingen gebeuren; je kan ook het bestand gaan bekijken met bv kwrite, en daar zal het er helemaal anders uitzien…

€Íÿ‚Æ&ÜáÒ²ÆŠÀ9ÿáUÿ2†ŒúKüۖ

De sleutel

Het volgende probleem is: hoe bepaal je de key? Hoe lang moet die zijn? Theorie buiten beschouwing gelaten kan je het zelf uitzoeken op jouw systeem met deze proefondervindelijke methode:

  
Public Sub TestKeyLength()

  Dim sKeyShorter As String
  
  sKeyShorter = "123456789012345678901234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  While (Len(sKeyShorter) > 1)
    sKeyShorter = Left(sKeyShorter, -1)
    Try hEncrypted = Cipher[sAlgorithm].Encrypt(sLine, sKeyShorter, sInitVector)
    If Not Error
      Print "no error at keylength: " & Str(Len(sKeyShorter))
      Exit
    Else
      Print "Err with: " & sKeyShorter & " " & Len(sKeyShorter)
    Endif
  Wend

End

Het resultaat in mijn console:

Hello cipher world 2
Geef een woord of tekst (einde met enter):
Gambas3 really rocks!
Key length does not match method with key 1234567890123456789012345
67890123
Err with: 123456789012345678901234567890ABCDEFGHIJKLMNOPQRSTUVWXY 5
5
Err with: 123456789012345678901234567890ABCDEFGHIJKLMNOPQRSTUVWX 54
Err with: 123456789012345678901234567890ABCDEFGHIJKLMNOPQRSTUVW 53
Err with: 123456789012345678901234567890ABCDEFGHIJKLMNOPQRSTUV 52
Err with: 123456789012345678901234567890ABCDEFGHIJKLMNOPQRSTU 51
Err with: 123456789012345678901234567890ABCDEFGHIJKLMNOPQRST 50
Err with: 123456789012345678901234567890ABCDEFGHIJKLMNOPQRS 49
Err with: 123456789012345678901234567890ABCDEFGHIJKLMNOPQR 48
Err with: 123456789012345678901234567890ABCDEFGHIJKLMNOPQ 47
Err with: 123456789012345678901234567890ABCDEFGHIJKLMNOP 46
Err with: 123456789012345678901234567890ABCDEFGHIJKLMNO 45
Err with: 123456789012345678901234567890ABCDEFGHIJKLMN 44
Err with: 123456789012345678901234567890ABCDEFGHIJKLM 43
Err with: 123456789012345678901234567890ABCDEFGHIJKL 42
Err with: 123456789012345678901234567890ABCDEFGHIJK 41
Err with: 123456789012345678901234567890ABCDEFGHIJ 40
Err with: 123456789012345678901234567890ABCDEFGHI 39
Err with: 123456789012345678901234567890ABCDEFGH 38
Err with: 123456789012345678901234567890ABCDEFG 37
Err with: 123456789012345678901234567890ABCDEF 36
Err with: 123456789012345678901234567890ABCDE 35
Err with: 123456789012345678901234567890ABCD 34
Err with: 123456789012345678901234567890ABC 33
Err with: 123456789012345678901234567890AB 32
Err with: 123456789012345678901234567890A 31
Err with: 123456789012345678901234567890 30
Err with: 12345678901234567890123456789 29
Err with: 1234567890123456789012345678 28
Err with: 123456789012345678901234567 27
Err with: 12345678901234567890123456 26
Err with: 1234567890123456789012345 25
Err with: 123456789012345678901234 24
Err with: 12345678901234567890123 23
Err with: 1234567890123456789012 22
Err with: 123456789012345678901 21
Err with: 12345678901234567890 20
Err with: 1234567890123456789 19
Err with: 123456789012345678 18
Err with: 12345678901234567 17
no error at keylength: 16
gM3/gsYm3OEC0g+ygcaKwDn/4VX/Mg6GjPqQS/zblgo=
I Decrypt /home/cybr/cipher.bin as : 
Gambas3 really rocks!

Dus in plaats van de 32 tekens voor de key, elders vermeld in literatuur, heb ik hier 16 nodig.

Ik kan aan mijn programma’s een tekstbestand leveren met een versleutelde toegangscode voor de databank.
Wie het bestand leest kan er niets mee. Wie het programma ontleedt wel als de sleutel en initvector (of salt) vast in de broncode staan.


De hele lijst van mogelijke algoritmes:
Continue reading

Foutenbeheer en log via Error klasse

Ik wil in mijn programma:

  • een vaste manier om met foutmeldingen te werken
  • nadien de foutmeldingen kunnen nazien

In allerlei Classes, Modules en Forms kunnen fouten ontstaan, ik hou altijd de laatste ter plaatse bij in een variabele sLastError.


PRIVATE sLastError AS String

PUBLIC SUB getLastError() AS String
  
  RETURN sLastError
  
END

In mijn FMain heb ik een procedure waar ik alle foutmeldingen naartoegooi:

FMain


PUBLIC SUB SetError(sTextline AS String) 
  
  lsbxError.Add(Str$(iErrCounter) & ": " & sTextline, MCommon.insertTop)
  IF MMain.bErrorFile 
    MMain.hError.writeError(Str$(iErrCounter) & ": " & sTextline)
  ENDIF 
  INC iErrCounter
  
END

Fouten zakken weg naar onder in het zichtbaar meldingen-schermdeel in de toepassing, de ListBox lsbxError.
De constante in MCommon die aangeeft dat bovenaan in de lijst wordt toegevoegd is misschien wat overdreven:

MCommon

PUBLIC CONST insertTop AS Integer = 0

In MMain heb ik mijn error object, wat alle foutmeldingen opvangt:

MMain

... 
hError = NEW CError  
  IF hError.sLastError
    bErrorFile = FALSE
    sLastError = "Error handling to file: " & hError.sLastError
  ELSE 
    bErrorFile = TRUE
  ENDIF 
...

Mijn foutklasse heet CError:

CError

PRIVATE $hErrorFile AS File
PUBLIC sLastError AS String
PRIVATE sErrorFileName AS String


PUBLIC SUB _new(OPTIONAL sFileName AS String)
  
  IF sFileName
    sErrorFileName = sFileName
    openErrorFile(sErrorFileName)
  ELSE 
    sErrorFileName = System.User.Home & "/.config/gambas" & "/" & Application.Name & "-error.txt"
    DEBUG sErrorFileName
    openErrorFile(sErrorFileName)
  ENDIF 
  
END

PUBLIC SUB getErrorFileName() AS String
  
  RETURN sErrorFileName

END


PUBLIC SUB openErrorFile(sErrorFile AS String) AS Boolean
  
  IF Exist(sErrorFile)
    'TRY $hErrorFile = OPEN sErrorFile FOR WRITE APPEND ' cannot append without maintenance
    TRY $hErrorFile = OPEN sErrorFile FOR WRITE CREATE  
  ELSE 
    TRY $hErrorFile = OPEN sErrorFile FOR WRITE CREATE 
  ENDIF 
  
  IF ERROR 
    sLastError = Error.Text
    RETURN FALSE
  ELSE 
    writeError("*** CError uses file: " & sErrorFileName)
    writeError("*** on: " & System.Host & " " & System.User.Name & " " & Application.Name & " " & Application.Version & " from " & Application.Path)
    sLastError = ""
    RETURN TRUE
  ENDIF 
  
END

PUBLIC SUB closeErrorFile()
  
  TRY CLOSE $hErrorFile
  TRY $hErrorFile.Close()
  
END


PUBLIC SUB writeError(sErrorText AS String) AS Boolean
  
  TRY PRINT #$hErrorFile, Date & "@" & Time & ":" & sErrorText
  IF ERROR 
    RETURN FALSE
  ELSE 
    RETURN TRUE
  ENDIF 
  
END

Upd 2018-04: ondertussen iets aangepaste versie:

' Gambas class file

' CError - help with formatting for display and storing to an error log

Public bLogToFile As Boolean

Private sPreviousError As String
Private sLastError As String
Private SErrorText As String

Private $hErrorFile As File
Private iErrorCount As Integer

Public Sub _new()
  
  Dim s As String
  
  ' rotate error log file
  If Exist(getErrorFileName() & "~")
    Try Kill getErrorFileName() & "~"
  Endif
  Try Copy getErrorFileName() To getErrorFileName() & "~"
  Try $hErrorFile = Open getErrorFileName() For Write Create
  If Not Error
    bLogToFile = True
    Exec ["gambas3", "-V"] To s
    s = "Error file for " & Application.Name & " " & Application.Version & " for " & System.User.Name & "@" & System.Host & ", gambas " & s
    Print #$hErrorFile, s 
    Print #$hErrorFile, " ----------------------------------------------------------------------------------------" 
    Try Close $hErrorFile
  Else
    Debug "try to open error file " & getErrorFileName() & ": " & Error.Text
    bLogToFile = False
  Endif
  iErrorCount = 0
  
End

Public Sub getErrorFileName() As String
  
  If Not Exist(System.User.Home &/ ".local/log")
    Mkdir System.User.Home &/ ".local/log/"
  Endif
  Return System.User.Home &/ ".local/log/" & Application.Name & "-err.txt"
  
End

Public Sub shiftError(sNewError As String)
 
  sPreviousError = sLastError
  sLastError = Stamp(sNewError)

End



Public Sub Stamp(s As String) As String
  
  Return Now() & ";" & Str(iErrorCount) & ": " & s
  
End


Public Sub setError(sError As String, Optional bForceLogToFile As Boolean) As String
  
  SErrorText = sError
  Inc iErrorCount
  shiftError(sError)
  If (bForceLogToFile Or bLogToFile)
    LogToFile(sLastError)
  Endif
  Return iErrorCount & " " & sError & " @" & Time(Now())
  
End


Public Sub getPreviousError() As String
  
  Return sPreviousError
  
End

Public Sub LogToFile(s As String)
  
  Try $hErrorFile = Open getErrorFileName() For Write Append
  If Error
    Debug Error.Text
  Endif
  Try Print #$hErrorFile, s
  If Error 
    Debug Error.Text
  Endif
  Try Close $hErrorFile
  If Error
    Debug Error.Text
  Endif
  
End

Versienummer onderhouden

Een Gambas programma heeft een versienummer dat bestaat uit x.x.x, bv 0.0.1 als je net een nieuw project gestart hebt.
Het is in de IDE zichtbaar in Project, properties, en kan daar ook aangepast worden. Het wordt automatische verhoogd als je “compile” doet in de IDE. In code kan je het opvragen met Application.Version.

In een programma dat een databank gebruikt, hou ik ook bij welke versie lokaal geïnstalleerd staat, en wat de vorige versie was. Dat kan helpen in het oplossen van problemen, bv op het moment dat de struktuur van de databank verandert, en je dus zeker moet zijn met welke versie je werkt. Eigenlijk zou ik ook in de databank de minimumversie moeten bewaren bedenk ik nu, of misschien zelfs loggen user_id, version_used.

Ik heb geprobeerd zicht te houden op de versies die lokaal gebruikt worden, opgeslagen in de Settings File (~/.config/gambas/application.conf):

application.conf

[General]
..
Language="nl_BE"
Runcounter=1028
..
PreviousVersion="0.2.38"
LastVersion="0.2.38"
Version="0.2.38"
HighVersion="0.2.39"
LowVersion="0.2.10"
PreVersionDateOut=07/01/2016 13:24:07.426
...

Ik kan aflezen wat de laagste gebruikte versie is, wat de hoogste (soms schakel je terug naar een lagere versie na vastgestelde fouten bv), wat de huidige, voorgaande en wat de laatst gebruikte versie was.

Waarvoor ik in de module MCommon:


PRIVATE sLastError AS String

PUBLIC SUB storeVersion()
  ' check version settings exist
  IF NOT Settings["General/PreviousVersion"]  ' previous version number used (lower)
    Settings["General/PreviousVersion"] = Application.Version 
  ENDIF 

  IF NOT Settings["General/LowVersion"] ' lowest version used 
    Settings["General/LowVersion"] = Application.Version
  ENDIF 
 
  IF NOT Settings["General/HighVersion"] ' highest version used - kept when downgrading!
    Settings["General/HighVersion"] = Application.Version
  ENDIF 
  
  Settings["General/LastVersion"] = Settings["General/Version", ""] ' first save value before replacing it
  Settings["General/Version"] = Application.Version ' store version of current run 
  
  IF (Settings["General/Version"] <> Settings["General/LastVersion"]) ' up or downgrade?
    IF newestVersion(Settings["General/LastVersion"], Settings["General/Version"]) = Settings["General/Version"] ' upgrade
      Settings["General/PreVersionDateOut"] = Now() 
      Settings["General/PreviousVersion"] = Settings["General/LastVersion"]
      
    ELSE ' downgrade, check under lowest ?
      IF newestVersion(Settings["General/LowVersion"], Application.Version) = Settings["General/LowVersion"]
        Settings["General/LowVersion"] = Application.Version
      ENDIF 
    ENDIF 
    IF Settings["General/HighVersion"] <> Application.Version
      Settings["General/HighVersion"] = newestVersion(Settings["General/HighVersion", ""], Application.Version)
    ENDIF 
    
  ENDIF 
  Settings.Save

END

met de test :

PUBLIC FUNCTION newestVersion(sVersionA AS String, sVersionB AS String) AS String
  
  DIM sPart AS String
  
  DIM arrsPartsA AS String[]
  DIM arrsPartsB AS String[]
  
  DIM i AS Integer
  
  IF NOT sVersionA ' sVersionA is empty
    IF sVersionB 
      RETURN sVersionB
    ELSE ' AND sVersionB is empty
      RETURN ""
    ENDIF 
  ELSE IF NOT sVersionB ' sVersionB is empty
    RETURN sVersionA
  ELSE ' none is empty; compare parts to find out
    arrsPartsA = Split(sVersionA, ".")
    arrsPartsB = Split(sVersionB, ".")
  
    IF arrsPartsA.Count = arrsPartsB.Count
  
      FOR i = 0 TO (arrsPartsA.Count - 1)
        IF Val(arrsPartsA[i]) > Val(arrsPartsB[i])
          RETURN sVersionA
        ELSE 
          IF Val(arrsPartsB[i]) > Val(arrsPartsA[i])
            RETURN sVersionB
          ELSE 
            sLastError = "Part " & Str$(i + 1) & " of version number is the same " & arrsPartsA[i]
            DEBUG sLastError
          ENDIF 
        ENDIF 
        
      NEXT 
    ELSE 
      sLastError = "Could not detect versions; structure not the same: " & sVersionA & " <> " & sVersionB
      RETURN ""
    ENDIF 
    
  ENDIF   
  
END

en aangeroepen vanuit de eerst opstartende module, bv de klasse MMain (die opstartklasse is en nadien FMain aanroept):

MMain


 MCommon.storeVersion()

Inleiding tot Gambas – PostgreSQL

Ik probeer het voorbeeld van dit artikel op Linux Journal na meer dan 10 jaar uit met één van de volgende versies van Gambas (3.x) en een andere databank die meer verschilt dan MySQL-MariaDB onderling: PostgreSQL.
Hier de beschrijving die gaat tot het ophalen en weergeven van gegevens (dus niet heel artikel uitgewerkt).

Vertrek van een Linux desktop systeem, met Gambas en PostgreSQL geïnstalleerd.
Het concept van het artikel is een soort summiere bug-tracker te maken.

Gambas IssueTracker
Start Gambas 3.9.x, new project, Database application.
– Je krijgt een lege FMain bij de “sources”.

Maak een knop om de applicatie te sluiten: kies onderaan rechts de “ok” knop uit de “Form” tab iconen, plak hem ergens op het scherm
– op het scherm staat een knop met “ok” erop.

Wijzig de tekst op de knop:
In de tab “Properties” rechts zie je een invulbaar veld naast “Text”; zet daar “Sluiten”.
– De knop op het scherm krijgt aangepaste tekst.
Zet helemaal bovenaan bij Name in plaats van Button1: btQuit

Wijzig de code van de knop:
Dubbelklik op de knop en je komt in de tekstbewerker. Tik tussen de begin en eindcode van de druk-op-de-knop procedure (die hij voor je klaarzet): Me.Close()
Samen ziet het er zo uit:

Public Sub btQuit_Click
  Me.Close()
End

Terwijl je op de run knop in de knoppenbalk van Gambas3 drukt (of F5 op je toetsenbord), weet je dat je een leeg venster op het scherm gaat zien met één knop: “Sluiten”. Je kan je niet bedwingen om op die knop te klikken.

Pauze om na te denken
Het oorspronkelijke artikel vermeldt volgende noden:

  • nieuw probleem melden
  • bijhouden wie het probleem meldde
  • probleem als opdracht toewijzen aan programmeur
  • de toestand van het probleem beheren
  • bijhouden wanneer het probleem werd gemeld
  • bijhouden in welke programmaversie het probleem is opgelost
  • overzicht krijgen van nieuwe problemen, problemen waar aan gewerkt wordt, en opgeloste problemen

Meer gedetailleerd hebben we nodig:

  • identificatie van de melder
  • identificatie van de oplosser
  • beschrijving van het probleem

Nog meer gedetailleerd:

  • gegevens van de melder: id, name
  • gegevens van de oplosser: id, name
  • details van het probleem: id, beschrijving, datum in, door wie aangebracht en opgelost, applicatie waarover ze gaat, versienummer van vaststelling probleem en oplossing

Databank

Om databank-layout uit te proberen is phpPgAdmin leuk, maar nadien weet je waarschijnlijk niet meer wat je juist gedaan hebt.
Het is handig om de databank aan te maken vanuit een bestand, zodat je met dat bestand gegarandeerd dezelfde databank kan maken.
(of je draait het om; maak in phpPgAdmin, en exporteer de vorm naar een bestand)

In MySQL/MariaDB was het zoiets:

Continue reading

Gebruik van verschillende Datasets

(DEV)
Programma’s waarbij je een database gebruikt, worden dikwijls ontwikkeld op een andere databank dan diegene die uiteindelijk zal gebruikt worden.
Stel dat de ontwikkeling gebeurt op server (DEV), met databank employee. Die server kan evengoed een virtuele machine, je eigen desktop of laptop zijn.

Structuur (DEV) –> (PRODUCTION)
Op een bepaald moment wordt het programma in gebruik genomen, en maak je dus een definitieve databank aan op de server (PRODUCTION), die werkelijk gebruikt wordt. Waarschijnlijk houd je je DEV voor verdere ontwikkeling, met een kleine test-database met relevante gegevens voor wat je aan het ontwikkelen bent, terwijl die van de (PRODUCTION) server met rasse schreden in volume toeneemt.

Data (PRODUCTION) –> (TEST)
Die verdere ontwikkeling testen met echte productie data wil je misschien laten doen door iemand anders, terwijl jij voortontwikkelt. Dan is het handig om een (TEST) systeem te hebben. De data kan je uit de productieserver halen, zodat je op grote aantallen data kan testen, wat realistischer is.

(DEV) ? (TEST) ? (PRODUCTION)
Als je nu vanop je ontwikkelomgeving programma’s wil gebruiken met data van TEST (vers geupdate van PRODUCTION), moet je de gegevens van je verbinding met de databank in het programma aanpassen. Waarschijnlijk in een settings- of configuratiebestand, wat het iets gemakkelijker maakt. Je kan dat settingsbestand dan in versies DEV en TEST maken, en met een shell script het te gebruiken configuratiebestand kopiëren over het standaard configuratiebestand. Of kan het vanuit je software?

Dataset
Concept: je kan in je programma voorzien om verschillende datasets te gebruiken. Gevolg is dat je in een menu kan omschakelen naar “dev”, “test”, of “production”, en dat de juiste connectiegegevens geladen worden.

Ik gebruik een aparte module voor de data (1)
MData

Ik nummer mijn datasets, zodat ik ze gemakkelijk kan aflopen maar ook in de Settings kan opslaan: 0, 1, 2…
Ik houd in een variabele in de data module bij welke dataset momenteel in gebruik is:
PRIVATE iCurrentDataSet AS Integer

Ik geef de dataset een standaard naam in de module die de data gebruikt:
PRIVATE CONST sDataSetName = "dbdata"

Voor de eigen data heb ik dus db configuraties:
dbdata0, dbdata1, dbdata2 (0-dev, 1-test, 2-production).

In de Settings file komt bv:
[dbdata0]
servertype="mysql"
datasetname="dbdatadev"
database="maindb"
host="localhost"
login="myapp"
password="ldfjq5sehrqz"
...

[dbdata1]
servertype="mysql"
datasetname="dbdatatest"
database="maindb"
host="test.copyleft.loc"
login="myapp"
password="ldfjq5sehrqz"
...

Om de instelling van de juiste dataset “sDataset” op te halen, te bewaren moet je hem natuurlijk kunnen benoemen:

Settings["dbdata1/host"]

Of via universelere code
sDataset & "/host"
waarbij
sDataset = getDataSetString(iCurrentDataSet)

Die is samengesteld van de naam plus het nummer, een procedure uit de betreffende module levert die:

PUBLIC SUB getDataSetString(i as Integer) AS String
   RETURN sDataSetName & Str$(iCurrentDataSet)
END

Hiermee kan je de voorkeuzelijst vullen.


NB:

Als je meer data modules gebruikt:

Een aparte module voor de data, import, export, enz, met telkens hetzelfde systeem.
MData
MImport
MExport

krijgt in elke module de dataset een *andere* naam:

dbdata = voor de eigen data van het programma
dbimport = voor data die geïmporteerd wordt uit een andere databank
..

Voor de import data heb ik dan datasets:
dbimport0, dbimport1, dbimport2

In de settingsfile zijn die dan terug te vinden:

[dbimport0]
servertype="mysql"
datasetname="dbimportdev"
database="maindb"
host="localhost"
login="readimport"
password="ldfjq5sehrqz"
...

[dbimport1]
servertype="mysql"
datasetname="dbimporttest"
database="maindb"
host="test.copyleft.loc"
login="readimport"
password="ldfjq5sehrqz"
...

Een aanzet tot ietwat universele code:

PUBLIC CONST sDataSet AS String = "dbimport" ' <---- Change this
' to change for each module - base for name in settings file

PRIVATE iCurrentDataSet AS Integer = 0 ' no of dataset, 0 is default, alternatives 1, 2, .. defined in settings

PRIVATE sCurDataSetName AS String ' 

PUBLIC $hconData AS NEW Connection



PUBLIC SUB getLastError() AS String
  
  RETURN sLastError
  
END

PUBLIC SUB resetError()
  
  sLastError = ""
  
END


PUBLIC SUB curDataSetString() AS String
  
  RETURN getDataSetString(iCurrentDataSet)
  
END

PRIVATE SUB getDataSetString(i AS Integer) AS String
  
  RETURN sDataSet & Str$(iCurrentDataSet)
  
END

PUBLIC SUB getDataSetName() AS String
  
  RETURN sCurDataSetName
  
END


PUBLIC SUB loadDataSet(iDataSet AS Integer) AS Boolean
  
  DIM sData AS String
  
  iCurrentDataSet = iDataSet
  sData = getDataSetString(iCurrentDataSet)
  
  DEBUG Settings.Path &/ Application.Name & ".conf"
  IF (Exist(Settings.Path &/ Application.Name & ".conf"))
    sCurDataSetName = Settings[sData & "/datasetname"]
    WITH $hconData
      .Host = Settings[sData & "/host"]
      .Name = Settings[sData & "/database"]
      .Login = Settings[sData & "/login"]
      .Password = Settings[sData & "/password"]
      .Type = Settings[sData & "/servertype"]
    END WITH 
    RETURN TRUE
  ELSE 
    iCurrentDataSet = -1
    sLastError = "Could not find data connection settings " & Error.Text
    RETURN FALSE
  ENDIF   
  
END


PUBLIC SUB goConnect() AS Boolean
  
  TRY $hconData.Close
  TRY $hconData.Open
  IF ERROR 
    sLastError = Error.Text
    RETURN FALSE
  ELSE 
    RETURN TRUE
  ENDIF 
  
END

Foutafhandeling (TRY – IF ERROR)

ps: Lijst van foutmeldingen gambas2 zijn hier nog terug te vinden:
http://files.allbasic.info/Gambas/help/help/error.html

Gambas heeft de TRY – IF ERROR combinatie.
Als de kans groot is dat zich een fout voordoet kan je de lijn beginnen met TRY. Als zich dan een fout voordoet crasht de applicatie niet, maar wordt akte genomen van de fout en loopt de applicatie door. Om waarschijnlijk verder toch in de problemen te komen?
Daarom is er de tweede stap: IF ERROR …
Deze instructie zet je na de lijn die begint met TRY.
Als er een fout is opgetreden, wordt de IF ERROR lijn uitgevoerd.

Dat is handig want je kan hier twee zaken mee doen:

  1. De foutmelding gaan ophalen, en er iets zinnigs mee doen, als loggen/opslaan, tonen, …
  2. De loop van het programma aanpassen aan het zich voordoen van de fout; kans geven om iets opnieuw te proberen, toestand van buttons / menu’s wijzigen zodat geen verdere akties genomen kunnen worden die de applicatie zouden doen crashen, een procedure starten om de fout effectief op te lossen, …

1. De foutmelding

De foutmelding zit in dat geval in Error.Text; je kan ze opvragen:

IF ERROR
  DEBUG Error.Text
ELSE
   ...
ENDIF

Terwijl je bezig bent met programmeren is het nuttig en nodig om de foutmeldingen te zien te krijgen. Als het programma crasht zie je ze. Soms wil je echter voortwerken aan een ander stuk van het programma, waarvoor deze fout niet belangrijk is. Je kan die dan “wegmoffelen” door voor de programmalijn die de fout levert, TRY te zetten. Het programma loopt na de fout door, maar: je krijgt de foutmelding niet meer te zien! Je ziet dus bv niet of ze verandert, zich niet meer zou voordoen, enz. Je vergeet ze misschien en dat levert laten een “bug” op.

Met deze DEBUG zie je ze tenminsten nog onderaan op je terminal-achtige venster in Gambas. Maar misschien is het beter ineens een procedure te maken om die fout ook in het lopende programma bij de gebruiker te gebruiken, want na compilatie zijn de DEBUG’s weg.

Eigenlijk is het minimum dat je ze opslaat in een tekstvariabele:

IF ERROR
  sLastError = Error.Text
...

Je kan dat lokaal doen, bv in elke module, Class, enz, lokaal een variabele maken om de fout op te slaan, en een procedure om ze op te vragen:

PRIVATE sLastError AS String
 
PUBLIC SUB getLastError() AS String
  RETURN sLastError
END


IF ERROR
  sLastError = Error.Text
...

De aanroepende code moet dan nog wel te weten komen dat er een fout was. Dat wordt meestal gedaan door
– een extreem andere waarde terug te geven als een waarde verwacht wordt (return value), bv 0 of -1.
– als geen return value nodig is, geef dan toch een TRUE als alles ok is, en FALSE terug als zich een fout voordeed.

In de aanroepende code:

IF module1.DoSomething(..)
  ...
ELSE
  Message.Error(module1.getLastError())
ENDIF

of, bv als je -1 als teken van fout gebruikt:

IF module1.CalcSomething(..) < 0
  Message.Error(module1.getLastError())
ELSE
  ...
ENDIF

Enkele ideeën:

  • Maak een procedure om de fouten op te vangen in je hoofdprogramma; bv doError(s AS String), die je de foutmelding laat weergeven in een speciaal foutmeldingsvenster in de app, en/of laat opslaan in een logbestand
  • Maak een class om de foutafhandeling te doen. Dan moet wel extra informatie door gegeven worden over waar de fout vandaan kwam.

Database velden met prefix van tabelnaam

Naar aanleiding van een vroeger artikel over hergebruik van code, kwam ik op de kwestie van de standaardisering van de veldnamen in een databank. Algemeen zie ik twee systemen gebruikt worden:

1. Met prefix:

Meestal een korte prefix die de naam van de tabel aangeeft, zodat je altijd kan zien waar de veldnaam op slaat:

# Name Type Null Default Extra
1 emp_id bigint(20) No None AUTO_INCREMENT
2 emp_name int(11) No 0
3 emp_data2 int(11) No 0
8 emp_creat timestamp Yes CURRENT_TIMESTAMP
9 emp_creby char(24) Yes NULL
10 emp_updat datetime Yes NULL
11 emp_updby char(24) Yes NULL

Inderdaad de employee tabel, “emp” of beter “employee”.

2. Zonder prefix

Zonder tabelnaam (afkorting) in veldnamen:

# Name Type Null Default Extra
1 id bigint(20) No None AUTO_INCREMENT
2 name int(11) No 0
3 data2 int(11) No 0
8 creat timestamp Yes CURRENT_TIMESTAMP
9 creby char(24) Yes NULL
10 updat datetime Yes NULL
11 updby char(24) Yes NULL

In dit geval moet je de (afkorting voor) de veldnaam niet meegeven. Als de database vermeld wordt is het toch duidelijk:

emp.id
employee.name
employee.fieldname1
employee.creat
employee.creby
employee.updat
employee.updby

eventueel met korte alias

emp.id
emp.name
emp.fieldname1
emp.creat
emp.creby
emp.updat
emp.updby

Veel code blijft korter, maar een luie blik op de resultaten van een query kan iets meer aandacht gaan vragen (om de oorspronkelijke tabelnaam te zoeken), en misschien worden je aliassen langer e.veld wordt emp.veld voor table employee.

Vgl:

e.emp_id, e.emp_name
emp.id, emp.name

De prefix blijkt vooral een overblijfsel te zijn van vroeger, toen alle veldnamen uniek moesten zijn (ook buiten de tabel).

De algemene raadgeving is: geen prefix gebruiken:

  • korter
  • eenduidiger
  • goede tabelnamen en aliassen te gebruiken
  • betere standaardisatie

Pro prefix:

  • oude systemen waar het moet
  • geen probleem met gereserveerde woorden (commanodo’s in de query taal)*
  • gemakkelijk bij export want tabelnaam staat in veld.
  • soms beter leesbaar omdat velden altijd dezelfde vorm hebben.

* stel dat je een veld hebt waarin je aangeeft dat er een update gebeurd is of moet gebeuren:
employee.update
Helaas, UPDATE is een gereserveerd woord in de SQL taal (SELECT update FROM …), en zo zijn er nog woorden die door de syntax highlighting kunnen aangewezen worden als “gereserveerd”. Door prefixen te gebruiken moet je daar nooit op letten. Maar je kan ook ‘quotes’ gebruiken rond het gereserveerd woord als veldnaam.

Zie ook discussie op StockOverflow: (1) en (2)