OpenEdge i VS Code 2

Każdy z Was zapewne często korzysta z pomocy ze strony sztucznej inteligencji. Warto więc powracać do tego tematu od czasu do czasu.
W Progress OpenEdge ten temat też jest aktywnie rozwijany, ale zanim pojawi się na tym blogu jakiś wpis na temat produktu OpenEdge MCP zobaczmy co możemy zrobić w omawianym niedawno środowisku VS Code.
Chodzi o agenta AI GitHub Copilot, który jest bardzi przydatny do pisania kodu “w locie”.
Jest to narzędzie wyspecjalizowane (tzw. AI pair programmer), które działa bezpośrednio w edytorze IDE, np. VS Code.
Jego zaletą jest błyskawiczne autouzupełnianie kodu i generowanie całych funkcji na podstawie komentarzy bez opuszczania edytora. Poza tym Copilot „widzi” pliki, nad którymi aktualnie pracujemy, dzięki czemu lepiej dopasowuje sugestie do naszego stylu i struktury projektu.

Do korzystania z Copilota w VS Code wymagane jest konto na GitHub. Jest ono niezbędne do uwierzytelnienia subskrypcji i synchronizacji ustawień. Od grudnia 2024 roku dostępna jest wersja Copilot Free, która pozwala na darmowe korzystanie z narzędzia w VS Code (oczywiście są limity).
Po zainstalowaniu rozszerzenia GitHub Copilot w VS Code, zostaniemy poproszeni o zalogowanie się właśnie przez GitHub. Przy logowaniu widzimy trzy opcje, ponieważ GitHub (należący do Microsoftu) wspiera tzw. logowanie społecznościowe.

Loguję się korzystając z mojego wcześniej utworzonegfo konta.

W oknie chata Copilota podaję przykładową instrukcję:
Write a program in openedge ABL that:
1. Prompts the user for a customer number
2. Finds that customer in the Customer table
3. Calculates the total value of all orders for that customer by summing OrderLine.ExtendedPrice
4. Displays the results.

Instrukcje możemy napisać po polsku ale warto jednak pamiętać, że choć Copilot rozumie polski, to jego główne dane treningowe pochodzą z publicznych repozytoriów, gdzie dominuje język angielski. Dlatego precyzyjne terminy techniczne lub bardziej złożone instrukcje mogą czasem dawać lepsze rezultaty w języku angielskim.
Musimy poczekać kilka minut. Poniżej jest wygenerowana procedura.

/* total_customer_orders.p
   Prompts for a customer number, finds the customer,
   sums OrderLine.ExtendedPrice for that customer's orders,
   and displays the total value.
*/

DEFINE VARIABLE iCustNo AS INTEGER NO-UNDO.
DEFINE VARIABLE decTotal AS DECIMAL NO-UNDO INITIAL 0.
DEFINE VARIABLE lHasLines AS LOGICAL NO-UNDO INITIAL FALSE.

PUT UNFORMATTED "Enter customer number: ".
IMPORT UNFORMATTED iCustNo.

FIND Customer NO-LOCK WHERE Customer.CustNum = iCustNo NO-ERROR.
IF NOT AVAILABLE Customer THEN DO:
    MESSAGE "Customer " iCustNo " not found." VIEW-AS ALERT-BOX.
    RETURN.
END.

FOR EACH Order NO-LOCK WHERE Order.CustNum = iCustNo NO-ERROR:
    FOR EACH OrderLine NO-LOCK WHERE OrderLine.OrderNum = Order.OrderNum NO-ERROR:
        ASSIGN decTotal = decTotal + OrderLine.ExtendedPrice
               lHasLines = TRUE.
    END.
END.

IF NOT lHasLines THEN
    MESSAGE "No orders found for customer " iCustNo VIEW-AS ALERT-BOX.
ELSE
    MESSAGE "Total value for customer " iCustNo ": " decTotal VIEW-AS ALERT-BOX.

RETURN.

Tu widzimy błędy jak np. NO-ERROR w pętli FOR EACH, czy sposób wprowadzenia numeru klienta. Stwórzmy plik z instrukcjami dla Copilota. Powinien on spowodować usunięcie NO-ERROR, zmienić sposób deklaracji zmiennych na nowocześniejszy a także wprowadzić pewne zmiany w formatowaniu.

.github/copilot-instructions.md

# Modern OpenEdge ABL Development Standards

## 1. Language Version
- Target OpenEdge ABL 12 or newer only.
- Do not generate legacy ABL syntax.

## 2. Variables
- Always use `VAR` for local variables.
- Never use `DEFINE VARIABLE`.
- Do not specify `NO-UNDO` (default in modern ABL).
- Use the equal sign (`=`) for initial values (e.g., `VAR INT i = 10`).

## 3. Data Types
- Use full data type names only:
  `CHARACTER`, `INTEGER`, `DECIMAL`, `LOGICAL`.
- Do not use abbreviated types.

## 4. Error Handling
- Every procedure or class must start with:
  `BLOCK-LEVEL ON ERROR UNDO, THROW.`

## 5. Formatting
- Use UPPERCASE for all ABL keywords.
- Use 4-space indentation.
- One statement per line.

## 6. Data Retrieval
- Don't use NO-ERROR with FOR loops.

Powtarzamy zlecenie najlepiej dla nowego czata.

/* total_customer_orders2.p
   Prompts for a customer number, finds the customer,
   sums OrderLine.ExtendedPrice for that customer's orders,
   and displays the total value.
*/

BLOCK-LEVEL ON ERROR UNDO, THROW.

VAR INTEGER iCustNo = 0.
VAR DECIMAL decTotal = 0.
VAR LOGICAL lHasLines = FALSE.

PUT UNFORMATTED "Enter customer number: ".
IMPORT UNFORMATTED iCustNo.

FIND Customer NO-LOCK WHERE Customer.CustNum = iCustNo NO-ERROR.
IF NOT AVAILABLE Customer THEN DO:
	MESSAGE "Customer " iCustNo " not found." VIEW-AS ALERT-BOX.
	RETURN.
END.

FOR EACH Order NO-LOCK WHERE Order.CustNum = iCustNo:
	FOR EACH OrderLine NO-LOCK WHERE OrderLine.OrderNum = Order.OrderNum:
		ASSIGN decTotal = decTotal + OrderLine.ExtendedPrice.
		ASSIGN lHasLines = TRUE.
	END.
END.

IF NOT lHasLines THEN
	MESSAGE "No orders found for customer " iCustNo VIEW-AS ALERT-BOX.
ELSE
	MESSAGE "Total value for customer " iCustNo ": " decTotal VIEW-AS ALERT-BOX.

RETURN.

Chciałbym żeby relacje były w skróconej formie ze słowem kluczowym OF, więc dodaję instrukcję na końcu pliku .github/copilot-instructions.md
– Establish relationships between two tables using the OF keyword

OK, trzecia próba wygenerowania procedury.

/* total_customer_orders3.p */
BLOCK-LEVEL ON ERROR UNDO, THROW.

VAR CHARACTER custNum = "".
VAR DECIMAL totalValue = 0.

DISPLAY "Enter Customer Number:".
UPDATE custNum.

FIND Customer WHERE Customer.CustNum = custNum NO-LOCK.

IF AVAILABLE Customer THEN
DO:
    FOR EACH Order OF Customer NO-LOCK:
        FOR EACH OrderLine OF Order NO-LOCK:
            totalValue = totalValue + OrderLine.ExtendedPrice.
        END.
    END.

    DISPLAY
        "Customer Number:" Customer.CustNum
        "Customer Name:" Customer.Name
        "Total Order Value:" totalValue.
END.
ELSE
DO:
    DISPLAY "Customer not found.".
END.

Chociaż nie instruowałem agenta o sposobie wprowadzenia numeru klienta, zmienił on sposób, ale niestety na błędny. Pole custnum nie może występować zanim rekord nie będzie w buforze, jednakże relacje sa napisane prawidłowo. Inny jest sposób podania wyniku co nie powinno nas dziwić, w końcu to sztuczna inteligencja.
No dobrze, ale co dostaniemy jeśli to zadanie skieruje do czataGPT. Nie zna on struktury bazy więc trzeba napisać, że chodzi o bazę testową sports2000.

/*---------------------------------------------------------
   Program: customerOrderTotal.p
   Purpose: Display total order value for a customer
---------------------------------------------------------*/

DEFINE VARIABLE iCustNum    AS INTEGER   NO-UNDO.
DEFINE VARIABLE dTotalValue AS DECIMAL   NO-UNDO.

/* Prompt user for customer number */
UPDATE iCustNum
    LABEL "Enter Customer Number"
    WITH FRAME fInput.

/* Find the customer */
FIND Customer
    WHERE Customer.CustNum = iCustNum
    NO-LOCK
    NO-ERROR.

IF NOT AVAILABLE Customer THEN DO:
    MESSAGE "Customer number" iCustNum "not found."
        VIEW-AS ALERT-BOX ERROR.
    RETURN.
END.

/* Calculate total order value */
FOR EACH Order
    WHERE Order.CustNum = Customer.CustNum
    NO-LOCK,
    EACH OrderLine
    WHERE OrderLine.OrderNum = Order.OrderNum
    NO-LOCK:

    dTotalValue = dTotalValue + OrderLine.ExtendedPrice.
END.

/* Display result */
MESSAGE
    "Customer:" Customer.Name SKIP
    "Total Order Value:" dTotalValue
    VIEW-AS ALERT-BOX INFORMATION.

Nie jest żle bo program jest kompilowalny. Można kazać mu stosować nowy sposób deklaracji zmiennych i skrócony zapis relacji między tablicami i wtedy będzie jeszcze lepiej. Chodzi tu jednak o dobrze znana bazę. Co jeśli mamy własne bazy i bardziej skomplikowany problem? No i co na to Progress? Czy nie ma dla nas jakiegoś bardziej eleganckiego rozwiązania? Prędzej czy później o tym napiszę.

Nowe typy extentów w bazach OpenEdge

Extenty są podstawowym elementem obszaru przechowywania (storage area) bazy danych OpenEdge. O ile bazę można podzielić na elementy logiczne jak tabele, indexy itp. to extenty są elementami fizycznymi – po prostu plikami na dysku.
Rozmiar, typ i lokalizację extentów definiuje się za pomocą pliku [nazwa_bazy].st.
Są dwa podstawowe typy extentów:

  • Extent stały (fixed extent)Umożliwia kontrolę nad tym, ile miejsca na dysku przydzielić każdemu extentowi oraz ile extentów użyć. Przestrzeń w takich extentach jest sformatowana podczas tworzenia struktury bazy przez co wydajność zapisu do nich jest lepsza. Administrator musi jednak monitorować stopień wypełnienie danych.
  • Extent zmienny (variable extent)Może przyrastać dynamicznie, stanowiąc taki “wentyl bezpieczeństwa” gdyby extenty stałe się zapełniły. Ułatwia to utrzymanie systemu, ponieważ nie trzeba regularnie monitorować rozmiaru extentów, ale powoduje spadek wydajności, gdyż OpenEdge RDBMS musi w trakcie zapisu do extentu zmiennego stale przydzielać i formatować przestrzeń.

    Co ważne, domyślnie ten typ extentu rośnie, aż osiągnie maksymalny rozmiar pliku dozwolony przez system operacyjny albo zajmie cały dysk lub system plików!
    Dlatego opcjonalnie można podać określony maksymalny rozmiar. Nie oznacza to jednak, że przestrzeń dla tego extentu jest przydzielona i sformatowana z góry (jak w przypadku extentu stałego). Po prostu rośnie on tylko do określonego rozmiaru.

Tak wygląda definicja przykładowych extentów w pliku .st bazy.

# extent stały
d "Cust_Data":9,32;1 . f 640
# extent zmienny z maksymalnym rozmiarem
d "Cust_Data":9,32;1 . v 1280

Wiadomo, że administrator powinien pilnować żeby dane w obszarze nie przekroczyły 80% jego objetości. Gdy sie to stannie należy dodać extenty stałe i oczywiście ostatni zmienny. Istniejący wtedy extent stały zmieni sie na stały ale jego rozmiar może być zupełnie inny od pozostałych zmiennych, często jest on bardzo mały jak extent .d3 na poniższym rysunku.

Nie jest to błędem ale po pewnym czasie możemy mieć wiele takich małych extentów w których będzie bardzo mało danych.
W OpenEdge 12 możemy temu zaradzić ponieważ dodano nowe tokeny do tworzenia i zarządzania zmiennymi extentami.

  • e rozszerza istniejący extent o zmiennej długości.
  • ev rozszerza istniejący extent o zmiennej długości i ustala maksymalny rozmiar.
  • x tworzy nowy, wstępnie rozszerzony extent o zmiennej długości.
  • xv tworzy nowy, wstępnie rozszerzony extent o zmiennej długości i ustala maksymalny rozmiar.

Np. w obszarze Cust_Data mamy dwa extenty, jeden stały i jeden zmienny jak poniżej.

Chcę dodać nowy extent zmienny z preformatowanym rozmiarem 320 kB i maksymalnym rozmiarem 640 kB, a istniejący zmienny zrobić preformatowany do wartości 320 kB. Extent ten będzie zamieniony na extent stały o określonej przez mnie długości. Plik t_add.st wygląda tak:

d "Cust_Data":9,32;1   t_9.d2   e   320  
d "Cust_Data":9,32;1   t_9.d3   xv  320  640

Uruchamiam komendę prostrct add z opcją -validate i jeśli format jest poprawny to ponownie ale już bez opcji -validate.


W pliku struktury t.st widać dodany extent. Extent .d2 ma tę samą długośc co .d1.

d "Cust_Data":9,32;1 C:\WrkOpenEdge128\db\t_9.d1 f 320
d "Cust_Data":9,32;1 C:\WrkOpenEdge128\db\t_9.d2 f 320
d "Cust_Data":9,32;1 C:\WrkOpenEdge128\db\t_9.d3 xv 320 640


A gdy wylistujemy strukturę bazy poleceniem prostrct list [baza] > mylist.st mamy dodatkowe informacje:

...
   Area Name: Cust_Data, Type 6, Block Size 4096, Extents 3, Records/Block 32, Cluster Size 1
   Ext # 1, Type FIXED, Size 320 KByte, Name: ...\t_9.d1
   Ext # 2, Type FIXED, Size 320 KByte, Name: ...\t_9.d2
   Ext # 3, Type PRE-EXTENDED VARIABLE w/MAX SIZE, Size 320 KByte, Name: ...\t_9.d3
   ...

Obie wartości pre-extended i maksimum rozmiaru pliku możemy usunąć poleceniem:
prostrct add [baza] add.st gdzie plik add.st wygląda następująco:

d "Cust_Data":9,32;1   t_9.d3   ev   0  0

Pamiętajmy, że nie ma flagi v, więc nie można usunąć tylko tej drugiej wartości.