The code:
*>=================================================================================
*> title : mysql-demo-native_104.cob (based upon rms-indexed-file-demo_102.bas)
*> authors : Antropic Claude (translation)
*> Neil Rieck (BASIC and COBOL development)
*> build : if mysql or mariadb development libraries not yet cached via ldconf
*> cobc -x -free -fstatic-call mysql-demo-native-104.cob -lmariadb
*> if mysql or mariadb development libraries where cached via ldconf
*> cobc -x -free mysql-demo-native-104.cob -lmariadb
*> history :
*> ver who when what
*> --- --- ---------- -------------------------------------------------------------
*> 100 NSR 2005-09-09 original demo (DEC-BASIC)
*> 101 NSR 2005-09-11 cleanup for public viewing (DEC-BASIC)
*> 102 AC 2026-09-30 translation of an demo ISAM DEC-BASIC to demo SQL-COBOL
*> 103 NSR 2026-10-11 changes to get this thing to run properly
*> 104 NSR 2026-10-11 changed all YN prompts to YNQ
*> --------------------------------------------------------------------------------
*> purpose: My original intention was to produce an SQL COBOL demo on Linux for a
*> friend. So on 2026-09-30 I asked Anthopic Claude the following:
*> "Please convert the following DEC-BASIC demo program to GnuCOBOL. Replace the
*> indexed file logic with with calls to MySQL"
*> Claude did not properly implement RFA which "I think" should have employed
*> an additional field based upon an autoincremented sequence number, so this demo
*> COBOL program only shows how to call MySQL or MariaDB client libraries.
*> Note: many third generation languages (COBOL, BASIC, etc) usually require a
*> small number of "C" files to glue things together. This demo does not.
*> --------------------------------------------------------------------------------
*> From Claude's conversion:
*> RMS -> MySQL mapping
*> primary key (last,first,city) -> UNIQUE KEY uk_primary
*> alternate key #1 last_name -> KEY idx_last
*> alternate key #2/#3 telephone -> KEY idx_tel (read DESC)
*> RFA (record file address) -> AUTO_INCREMENT column rfa
*> RMS error 134 (dup key) -> MySQL error 1062
*>
*> note : MySQL connections default to autocommit, so no
*> explicit COMMIT calls are made.
*>=================================================================================
IDENTIFICATION DIVISION.
PROGRAM-ID. MYSQL-DEMO-NATIVE.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 K-PROGRAM PIC X(30) VALUE "MySQL-Demo-Native-104".
*> connection parameters (null-terminated for the C API)
01 DB-HOST PIC X(32) VALUE Z"neilrieck.net". *> Z means null-terminate
01 DB-USER PIC X(32) VALUE Z"bdb_demo_user". *>
01 DB-PWD PIC X(32) VALUE Z"bdb_demo_pass". *>
01 DB-NAME PIC X(32) VALUE Z"bdb_demo_db". *>
01 DB-TABLE PIC X(12) VALUE "bdb_demo_tbl". *> do not null-terminate this one
01 DB-PORT BINARY-LONG UNSIGNED VALUE 3306.
01 DB-FLAGS BINARY-C-LONG UNSIGNED VALUE 0.
*> C API handles and return values
01 WS-NULL USAGE POINTER VALUE NULL.
01 WS-CONN USAGE POINTER VALUE NULL.
01 WS-CONN-OK USAGE POINTER VALUE NULL.
01 WS-RES USAGE POINTER VALUE NULL.
01 WS-ROW-PTR USAGE POINTER VALUE NULL.
01 WS-ERR-PTR USAGE POINTER VALUE NULL.
01 WS-RC BINARY-LONG VALUE 0.
01 WS-ERRNO BINARY-LONG UNSIGNED VALUE 0.
01 WS-AFFECTED BINARY-DOUBLE UNSIGNED VALUE 0.
01 WS-AFFECTED-DISP PIC Z(8)9.
*> SQL text builder
01 WS-SQL PIC X(1200).
01 WS-PTR PIC 9(4) COMP-5.
01 WS-ORDER-BY PIC X(60).
*> general work fields
01 WS-JUNK PIC X(80).
01 WS-PROMPT PIC X(70).
01 WS-ANSWER PIC X.
88 ANSWER-YES VALUE "Y".
88 ANSWER-NO VALUE "N".
88 ANSWER-QUIT VALUE "Q".
01 ANSWER-CHOICES PIC X(3) VALUE "YNQ".
01 WS-TEMP PIC 9(3).
01 WS-INDEX PIC 9(3).
01 WS-LENGTH PIC 9(3).
01 WS-CHAR PIC X.
01 WS-REC-COUNT PIC 9(4) VALUE 0.
01 WS-REC-DISP PIC Z(3)9.
01 WS-ERR-PHASE PIC X(10).
01 WS-ERRTXT PIC X(256).
01 WS-ERRNO-DISP PIC Z(8)9.
01 WS-SLEEP PIC 9 VALUE 1.
01 WS-IDX PIC 9.
01 WS-FIELD PIC X(60).
01 WS-DB-STATUS PIC 9 VALUE 0.
88 DB-OK VALUE 0.
88 DB-FAILED VALUE 1.
01 WS-CONNECTED PIC 9 VALUE 0.
88 IS-CONNECTED VALUE 1.
*> the "disk record" (the BASIC MAP)
01 D21-REC.
05 D21-FIRST-NAME PIC X(20).
05 D21-LAST-NAME PIC X(20).
05 D21-TELEPHONE PIC X(10).
05 D21-ADDRESS PIC X(20).
05 D21-CITY PIC X(20).
05 D21-POSTAL-CODE PIC X(10).
05 FILLER PIC X(50).
01 WS-FIND-LAST PIC X(20).
01 WS-FIND-FIRST PIC X(20).
01 WS-RFA PIC X(20).
LINKAGE SECTION.
*> a result row is an array of char* (one per column)
01 ROW-ARR.
05 ROW-FLD USAGE POINTER OCCURS 6 TIMES.
*> overlay used to read any C string returned by the library
01 FLD-BUF PIC X(512).
PROCEDURE DIVISION.
*> ===========================================================
*> main
*> ===========================================================
MAIN-LINE.
DISPLAY K-PROGRAM
DISPLAY "=============================="
MOVE "OK to delete 'demo data table'?" TO WS-PROMPT
PERFORM ASK-YNQ
IF ANSWER-QUIT GO TO SORTIE END-IF
IF ANSWER-YES
PERFORM DB-CONNECT
IF DB-FAILED GO TO SORTIE END-IF
PERFORM NEW-SQL
STRING "DROP TABLE IF EXISTS " DB-TABLE
DELIMITED BY SIZE INTO WS-SQL WITH POINTER WS-PTR
MOVE "drop" TO WS-ERR-PHASE
PERFORM RUN-SQL
IF DB-FAILED GO TO SORTIE END-IF
END-IF
STRING "OK to create/open '" DB-TABLE "'?" INTO WS-PROMPT
PERFORM ASK-YNQ
IF ANSWER-QUIT GO TO SORTIE END-IF
IF ANSWER-YES
PERFORM CREATE-TABLE
IF DB-FAILED GO TO SORTIE END-IF
END-IF
MOVE "OK to write 3 demo data records?" TO WS-PROMPT
PERFORM ASK-YNQ
IF ANSWER-QUIT GO TO SORTIE END-IF
IF ANSWER-YES
PERFORM WRITE-RECORDS
PERFORM READ-SEQUENTIALLY
END-IF
MOVE "OK to display records in reverse telephone order?"
TO WS-PROMPT
PERFORM ASK-YNQ
IF ANSWER-QUIT GO TO SORTIE END-IF
IF ANSWER-YES
MOVE "telephone DESC" TO WS-ORDER-BY
MOVE "phase 4" TO WS-ERR-PHASE
DISPLAY "-i- reading table: " DB-TABLE " by telephone DESC"
PERFORM LIST-ALL
END-IF
MOVE "OK to delete record for 'Dave Cutler'?" TO WS-PROMPT
PERFORM ASK-YNQ
IF ANSWER-QUIT GO TO SORTIE END-IF
IF ANSWER-YES
MOVE "Cutler" TO WS-FIND-LAST
MOVE "Dave" TO WS-FIND-FIRST
PERFORM DELETE-BY-NAME
PERFORM READ-SEQUENTIALLY
END-IF
MOVE "OK to delete 'Gordon Bell' using the RFA method?"
TO WS-PROMPT
PERFORM ASK-YNQ
IF ANSWER-QUIT GO TO SORTIE END-IF
IF ANSWER-YES
PERFORM DELETE-BY-RFA
PERFORM READ-SEQUENTIALLY
END-IF
MOVE "OK to change Ken Olsen's City?" TO WS-PROMPT
PERFORM ASK-YNQ
IF ANSWER-QUIT GO TO SORTIE END-IF
IF ANSWER-YES
PERFORM UPDATE-OLSEN
PERFORM READ-SEQUENTIALLY
END-IF
DISPLAY "------------------------------------------------------------"
DISPLAY "That's all for now"
CALL "C$SLEEP" USING WS-SLEEP
GO TO SORTIE.
*> ===========================================================
*> DB-CONNECT : mysql_init + mysql_real_connect
*> ===========================================================
DB-CONNECT.
SET DB-OK TO TRUE
CALL "mysql_init" USING BY VALUE WS-NULL
RETURNING WS-CONN
IF WS-CONN = NULL
DISPLAY "-e- mysql_init failed (out of memory?)"
SET DB-FAILED TO TRUE
ELSE
CALL "mysql_real_connect"
USING BY VALUE WS-CONN
BY REFERENCE DB-HOST
BY REFERENCE DB-USER
BY REFERENCE DB-PWD
BY REFERENCE DB-NAME
BY VALUE DB-PORT
BY VALUE WS-NULL
BY VALUE DB-FLAGS
RETURNING WS-CONN-OK
IF WS-CONN-OK = NULL
MOVE "connect" TO WS-ERR-PHASE
PERFORM SHOW-DB-ERROR
SET DB-FAILED TO TRUE
ELSE
SET IS-CONNECTED TO TRUE
END-IF
END-IF.
*> ===========================================================
*> CREATE-TABLE : equivalent of the BASIC OPEN ...
*> ORGANIZATION INDEXED with its primary and alternate keys
*> ===========================================================
CREATE-TABLE.
DISPLAY "-i- opening table: " DB-TABLE
PERFORM NEW-SQL
STRING "CREATE TABLE IF NOT EXISTS " DB-TABLE " ("
"rfa BIGINT AUTO_INCREMENT PRIMARY KEY,"
"first_name VARCHAR(20) NOT NULL DEFAULT '',"
"last_name VARCHAR(20) NOT NULL DEFAULT '',"
"telephone VARCHAR(10) NOT NULL DEFAULT '',"
"address VARCHAR(20) NOT NULL DEFAULT '',"
"city VARCHAR(20) NOT NULL DEFAULT '',"
"postal_code VARCHAR(10) NOT NULL DEFAULT '',"
"UNIQUE KEY uk_primary (last_name,first_name,city),"
"KEY idx_last (last_name),"
"KEY idx_tel (telephone)"
") ENGINE=InnoDB"
DELIMITED BY SIZE INTO WS-SQL WITH POINTER WS-PTR
MOVE "phase 1" TO WS-ERR-PHASE
PERFORM RUN-SQL.
*> ===========================================================
*> WRITE-RECORDS : four PUTs; the 4th repeats the 3rd record
*> to provoke a duplicate-key error (RMS 134 / MySQL 1062)
*> ===========================================================
WRITE-RECORDS.
MOVE 0 TO WS-REC-COUNT
DISPLAY "-i- writing table: " DB-TABLE
INITIALIZE D21-REC
MOVE "Ken" TO D21-FIRST-NAME
MOVE "Olsen" TO D21-LAST-NAME
MOVE "4165553333" TO D21-TELEPHONE
MOVE "129 Parker Street" TO D21-ADDRESS
MOVE "Toronto" TO D21-CITY
MOVE "01754" TO D21-POSTAL-CODE
PERFORM PUT-RECORD
MOVE "Dave" TO D21-FIRST-NAME
MOVE "Cutler" TO D21-LAST-NAME
MOVE "4165552222" TO D21-TELEPHONE
MOVE "220 Simcoe Street" TO D21-ADDRESS
MOVE "Toronto" TO D21-CITY
MOVE "M5T1T4" TO D21-POSTAL-CODE
PERFORM PUT-RECORD
MOVE "Gordon" TO D21-FIRST-NAME
MOVE "Bell" TO D21-LAST-NAME
MOVE "4165551111" TO D21-TELEPHONE
MOVE "483 Bay Street" TO D21-ADDRESS
MOVE "Toronto" TO D21-CITY
MOVE "M5G2C9" TO D21-POSTAL-CODE
PERFORM PUT-RECORD
DISPLAY "-i- will rewrite previous record to force a "
"duplicate key error"
PERFORM PUT-RECORD
MOVE WS-REC-COUNT TO WS-REC-DISP
DISPLAY "-i- recs : " WS-REC-DISP.
PUT-RECORD.
COMPUTE WS-REC-DISP = WS-REC-COUNT + 1
DISPLAY "-i- writing record: " WS-REC-DISP
PERFORM NEW-SQL
STRING "INSERT INTO " DB-TABLE " "
"(first_name,last_name,telephone,"
"address,city,postal_code) VALUES ('"
FUNCTION TRIM(D21-FIRST-NAME TRAILING) "','"
FUNCTION TRIM(D21-LAST-NAME TRAILING) "','"
FUNCTION TRIM(D21-TELEPHONE TRAILING) "','"
FUNCTION TRIM(D21-ADDRESS TRAILING) "','"
FUNCTION TRIM(D21-CITY TRAILING) "','"
FUNCTION TRIM(D21-POSTAL-CODE TRAILING) "')"
DELIMITED BY SIZE INTO WS-SQL WITH POINTER WS-PTR
MOVE "phase 2" TO WS-ERR-PHASE
PERFORM RUN-SQL
IF DB-OK
ADD 1 TO WS-REC-COUNT
END-IF.
*> ===========================================================
*> READ-SEQUENTIALLY : primary-key order (key #0)
*> ===========================================================
READ-SEQUENTIALLY.
MOVE "OK to display data records sequentially?"
TO WS-PROMPT
PERFORM ASK-YNQ
IF ANSWER-YES
DISPLAY "-i- reading table: " DB-TABLE " sequentially"
MOVE "last_name, first_name, city" TO WS-ORDER-BY
MOVE "phase 3" TO WS-ERR-PHASE
PERFORM LIST-ALL
END-IF.
*> ===========================================================
*> LIST-ALL : SELECT all rows ORDER BY WS-ORDER-BY and display
*> ===========================================================
LIST-ALL.
PERFORM NEW-SQL
STRING "SELECT first_name,last_name,telephone,address,"
"city,postal_code FROM " DB-TABLE" ORDER BY "
FUNCTION TRIM(WS-ORDER-BY TRAILING)
DELIMITED BY SIZE INTO WS-SQL WITH POINTER WS-PTR
PERFORM RUN-SQL
IF DB-OK
CALL "mysql_store_result" USING BY VALUE WS-CONN
RETURNING WS-RES
IF WS-RES = NULL
PERFORM SHOW-DB-ERROR
ELSE
PERFORM FETCH-ROW
PERFORM UNTIL WS-ROW-PTR = NULL
PERFORM SHOW-RECORD
PERFORM FETCH-ROW
END-PERFORM
DISPLAY "-i- end of data (RMS 11 equivalent)"
CALL "mysql_free_result" USING BY VALUE WS-RES
END-IF
END-IF.
*> fetch next row; WS-ROW-PTR = NULL at end of data
FETCH-ROW.
CALL "mysql_fetch_row" USING BY VALUE WS-RES
RETURNING WS-ROW-PTR
IF WS-ROW-PTR NOT = NULL
SET ADDRESS OF ROW-ARR TO WS-ROW-PTR
PERFORM LOAD-ROW
END-IF.
*> copy the six C strings into the record buffer
LOAD-ROW.
MOVE 1 TO WS-IDX
PERFORM GET-FIELD
MOVE WS-FIELD TO D21-FIRST-NAME
MOVE 2 TO WS-IDX
PERFORM GET-FIELD
MOVE WS-FIELD TO D21-LAST-NAME
MOVE 3 TO WS-IDX
PERFORM GET-FIELD
MOVE WS-FIELD TO D21-TELEPHONE
MOVE 4 TO WS-IDX
PERFORM GET-FIELD
MOVE WS-FIELD TO D21-ADDRESS
MOVE 5 TO WS-IDX
PERFORM GET-FIELD
MOVE WS-FIELD TO D21-CITY
MOVE 6 TO WS-IDX
PERFORM GET-FIELD
MOVE WS-FIELD TO D21-POSTAL-CODE.
*> read the null-terminated string at ROW-FLD(WS-IDX)
GET-FIELD.
MOVE SPACES TO WS-FIELD
IF ROW-FLD(WS-IDX) NOT = NULL
SET ADDRESS OF FLD-BUF TO ROW-FLD(WS-IDX)
UNSTRING FLD-BUF DELIMITED BY X"00" INTO WS-FIELD
END-IF.
SHOW-RECORD.
DISPLAY "first name : " D21-FIRST-NAME
DISPLAY "last_name : " D21-LAST-NAME
DISPLAY "telephone : " D21-TELEPHONE
DISPLAY "address : " D21-ADDRESS
DISPLAY "city : " D21-CITY
DISPLAY "postal code : " D21-POSTAL-CODE
DISPLAY "=============================="
CALL "C$SLEEP" USING WS-SLEEP.
*> ===========================================================
*> DELETE-BY-NAME : delete every row matching the name
*> ===========================================================
DELETE-BY-NAME.
PERFORM NEW-SQL
STRING "DELETE FROM " DB-TABLE " WHERE last_name='"
FUNCTION TRIM(WS-FIND-LAST TRAILING)
"' AND first_name='"
FUNCTION TRIM(WS-FIND-FIRST TRAILING) "'"
DELIMITED BY SIZE INTO WS-SQL WITH POINTER WS-PTR
MOVE "phase 5" TO WS-ERR-PHASE
PERFORM RUN-SQL
IF DB-OK
PERFORM SHOW-AFFECTED
END-IF.
*> ===========================================================
*> DELETE-BY-RFA : fetch the row id (RFA) via the name index,
*> then delete using only that id
*> ===========================================================
DELETE-BY-RFA.
PERFORM NEW-SQL
STRING "SELECT rfa FROM " DB-TABLE " WHERE "
"last_name='Bell' AND first_name='Gordon' LIMIT 1"
DELIMITED BY SIZE INTO WS-SQL WITH POINTER WS-PTR
MOVE "phase 6" TO WS-ERR-PHASE
PERFORM RUN-SQL
IF DB-OK
CALL "mysql_store_result" USING BY VALUE WS-CONN
RETURNING WS-RES
IF WS-RES = NULL
PERFORM SHOW-DB-ERROR
ELSE
CALL "mysql_fetch_row" USING BY VALUE WS-RES
RETURNING WS-ROW-PTR
IF WS-ROW-PTR = NULL
DISPLAY "-e- record not found "
"(RMS 155 equivalent)"
CALL "mysql_free_result" USING BY VALUE WS-RES
ELSE
SET ADDRESS OF ROW-ARR TO WS-ROW-PTR
MOVE 1 TO WS-IDX
PERFORM GET-FIELD
MOVE WS-FIELD TO WS-RFA
CALL "mysql_free_result" USING BY VALUE WS-RES
PERFORM NEW-SQL
STRING "DELETE FROM " DB-TABLE " WHERE rfa="
FUNCTION TRIM(WS-RFA TRAILING)
DELIMITED BY SIZE
INTO WS-SQL WITH POINTER WS-PTR
PERFORM RUN-SQL
IF DB-OK
DISPLAY "-i- deleted by RFA " WS-RFA
PERFORM SHOW-AFFECTED
END-IF
END-IF
END-IF
END-IF.
*> ===========================================================
*> UPDATE-OLSEN : change city/postal code. In RMS this fails
*> with 130 because city is part of the primary key and needs
*> delete+put. SQL simply updates the row in place.
*> ===========================================================
UPDATE-OLSEN.
PERFORM NEW-SQL
STRING "UPDATE " DB-TABLE " SET city='Maynard',"
"postal_code='' WHERE first_name='Ken' "
"AND last_name='Olsen' AND city='Toronto'"
DELIMITED BY SIZE INTO WS-SQL WITH POINTER WS-PTR
MOVE "phase 7" TO WS-ERR-PHASE
PERFORM RUN-SQL
IF DB-OK
PERFORM SHOW-AFFECTED
END-IF.
*> ===========================================================
*> SQL helpers
*> ===========================================================
NEW-SQL.
MOVE SPACES TO WS-SQL
MOVE 1 TO WS-PTR.
*> terminate the string and execute it
RUN-SQL.
SET DB-OK TO TRUE
STRING X"00" DELIMITED BY SIZE
INTO WS-SQL WITH POINTER WS-PTR
CALL "mysql_query" USING BY VALUE WS-CONN
BY REFERENCE WS-SQL
RETURNING WS-RC
IF WS-RC NOT = 0
PERFORM SHOW-DB-ERROR
SET DB-FAILED TO TRUE
END-IF.
SHOW-AFFECTED.
CALL "mysql_affected_rows" USING BY VALUE WS-CONN
RETURNING WS-AFFECTED
MOVE WS-AFFECTED TO WS-AFFECTED-DISP
IF WS-AFFECTED = 0
DISPLAY "-e- no matching record (RMS 155 equivalent)"
ELSE
DISPLAY "-i- rows affected: " WS-AFFECTED-DISP
END-IF.
SHOW-DB-ERROR.
CALL "mysql_errno" USING BY VALUE WS-CONN
RETURNING WS-ERRNO
CALL "mysql_error" USING BY VALUE WS-CONN
RETURNING WS-ERR-PTR
MOVE SPACES TO WS-ERRTXT
IF WS-ERR-PTR NOT = NULL
SET ADDRESS OF FLD-BUF TO WS-ERR-PTR
UNSTRING FLD-BUF DELIMITED BY X"00" INTO WS-ERRTXT
END-IF
MOVE WS-ERRNO TO WS-ERRNO-DISP
DISPLAY "-e- error: " WS-ERRNO-DISP " in " WS-ERR-PHASE
DISPLAY "-i- text : " FUNCTION TRIM(WS-ERRTXT)
IF WS-ERRNO = 1062
DISPLAY "-i- duplicate key (RMS 134 equivalent)"
END-IF.
*> ===========================================================
*> utilities
*> ===========================================================
ASK-YNQ.
DISPLAY FUNCTION TRIM(WS-PROMPT TRAILING) " (y/n/q) " WITH NO ADVANCING
MOVE SPACES TO WS-JUNK
ACCEPT WS-JUNK
MOVE FUNCTION UPPER-CASE(FUNCTION TRIM(WS-JUNK LEADING))
TO WS-ANSWER.
COMPUTE WS-LENGTH = FUNCTION LENGTH(ANSWER-CHOICES)
MOVE 0 to WS-INDEX
PERFORM VARYING WS-TEMP FROM 1 BY 1 UNTIL
((WS-TEMP > WS-LENGTH) OR (WS-INDEX > 0))
MOVE ANSWER-CHOICES(WS-TEMP:1) TO WS-CHAR
IF WS-CHAR = WS-ANSWER
MOVE WS-TEMP TO WS-INDEX
END-IF
END-PERFORM
IF WS-INDEX = 0
DISPLAY "-e-illegal choice"
GO TO ASK-YNQ
END-IF.
*> ===========================================================
*> that's all folks
*> ===========================================================
SORTIE.
IF IS-CONNECTED
CALL "mysql_close" USING BY VALUE WS-CONN
END-IF
DISPLAY "Adios..."
STOP RUN.