Skip to main content

ironwork_exec/
sql.rs

1//! The SQL runtime is `rt::sql`, with the host type of a declared item from `compile::sql`.
2
3pub use compile::sql::{host_array, host_type};
4pub use rt::sql::*;
5
6#[cfg(test)]
7mod tests {
8    use super::*;
9    use crate::Execute;
10    use crate::layout::Resolved;
11
12    fn types(data: &str, names: &[&str]) -> Vec<Result<HostType, String>> {
13        let source = format!(
14            "       IDENTIFICATION DIVISION.\n       PROGRAM-ID. T.\n       DATA DIVISION.\n       WORKING-STORAGE SECTION.\n{data}       PROCEDURE DIVISION.\n           GOBACK.\n"
15        );
16        let compiled = crate::compile(syntax::parse(&source).expect("parses"), &[]).expect("compiles");
17        let layout = &compiled.layout;
18        names
19            .iter()
20            .map(|n| match layout.resolve(n, &[], syntax::Pos::default()) {
21                Ok(Resolved::Item(i)) => host_type(layout, i),
22                other => panic!("{n}: {other:?}"),
23            })
24            .collect()
25    }
26
27    #[test]
28    fn cobol_declarations_as_db2_reads_them() {
29        let got = types(
30            concat!(
31                "       01 H PIC S9(4) COMP.\n",
32                "       01 I PIC S9(9) COMP-5.\n",
33                "       01 B PIC S9(18) BINARY.\n",
34                "       01 D PIC S9(5)V99 COMP-3.\n",
35                "       01 Z PIC S9(5)V99 SIGN LEADING SEPARATE.\n",
36                "       01 C PIC X(10).\n",
37                "       01 F COMP-2.\n",
38                "       01 V.\n",
39                "          49 V-LEN PIC S9(4) COMP.\n",
40                "          49 V-TEXT PIC X(30).\n",
41                "       01 S.\n",
42                "          05 S-ID PIC S9(9) COMP.\n",
43                "          05 S-NAME PIC X(20).\n",
44            ),
45            &["H", "I", "B", "D", "Z", "C", "F", "V", "S"],
46        );
47        assert_eq!(got[0], Ok(HostType::SmallInt { signed: true }));
48        assert_eq!(got[1], Ok(HostType::Integer { signed: true }));
49        assert_eq!(got[2], Ok(HostType::BigInt { signed: true }));
50        assert_eq!(got[3], Ok(HostType::Decimal { digits: 7, scale: 2, signed: true }));
51        assert!(matches!(got[4], Ok(HostType::Zoned { digits: 7, scale: 2, signed: true, sign: Some(_) })));
52        assert_eq!(got[5], Ok(HostType::Char(10)));
53        assert_eq!(got[6], Ok(HostType::Double));
54        assert_eq!(got[7], Ok(HostType::VarChar(30)));
55        let Ok(HostType::Structure(members)) = &got[8] else { panic!("{:?}", got[8]) };
56        assert_eq!(members.iter().map(|(_, t)| t.clone()).collect::<Vec<_>>(), [HostType::Integer { signed: true }, HostType::Char(20)]);
57    }
58
59    #[test]
60    fn dbcs_items_bind_as_graphic_and_vargraphic() {
61        let data = "       01 G PIC G(5) DISPLAY-1.\n       01 V.\n          49 V-LEN PIC S9(4) COMP.\n          49 V-TEXT PIC G(30) DISPLAY-1.\n       01 E PIC GBG DISPLAY-1.\n";
62        let got = types(data, &["G", "V", "E"]);
63        assert_eq!((&got[0], &got[1]), (&Ok(HostType::Graphic(5)), &Ok(HostType::VarGraphic(30))));
64        assert!(got[2].is_err(), "a DBCS PICTURE with B has no SQL type");
65    }
66
67    #[test]
68    fn declarations_with_no_sql_type() {
69        let got = types("       01 E PIC ZZ9.99.\n       01 P USAGE POINTER.\n       01 BS PIC S9(3)V9 COMP.\n", &["E", "P", "BS"]);
70        assert!(got.iter().all(Result::is_err), "{got:?}");
71    }
72
73    /// Against a live server named by IRONWORK_PG_URL, which `tools/pg-test.sh` starts in a
74    /// container; without it the test passes without running.
75    mod live {
76        use super::*;
77        use std::cell::RefCell;
78        use std::rc::Rc;
79
80        fn url() -> Option<String> {
81            std::env::var("IRONWORK_PG_URL").ok()
82        }
83
84        const SCHEMA: &str = "DROP TABLE IF EXISTS emp; \
85            CREATE TABLE emp (id integer PRIMARY KEY, name char(10) NOT NULL, amt numeric(7,2), stamp timestamp); \
86            INSERT INTO emp VALUES (1, 'ADAMS', 100.50, '2020-01-02 03:04:05.5'), (2, 'BAKER', NULL, NULL), (3, 'CLARK', 30, NULL); \
87            CREATE SCHEMA IF NOT EXISTS sysibm; \
88            CREATE OR REPLACE VIEW sysibm.sysdummy1 AS SELECT 'Y'::char(1) AS ibmreqd;";
89
90        const PROGRAM: &str = concat!(
91            "       IDENTIFICATION DIVISION.\n",
92            "       PROGRAM-ID. PGQ5.\n",
93            "       DATA DIVISION.\n",
94            "       WORKING-STORAGE SECTION.\n",
95            "           EXEC SQL INCLUDE SQLCA END-EXEC.\n",
96            "       01 WS-ID    PIC S9(9) COMP VALUE 2.\n",
97            "       01 WS-NAME  PIC X(10).\n",
98            "       01 WS-AMT   PIC S9(5)V99 COMP-3.\n",
99            "       01 WS-IND   PIC S9(4) COMP.\n",
100            "       01 WS-STAMP PIC X(26).\n",
101            "       01 WS-Y     PIC X.\n",
102            "       01 WS-COUNT PIC S9(9) COMP.\n",
103            "       01 E-AMT    PIC -9(5).99.\n",
104            "       01 E-CODE   PIC -9(3).\n",
105            "       01 E-NUM    PIC -9(3).\n",
106            "           EXEC SQL DECLARE C1 CURSOR FOR\n",
107            "                SELECT NAME, AMT FROM EMP WHERE ID >= :WS-ID\n",
108            "                ORDER BY ID\n",
109            "           END-EXEC.\n",
110            "       PROCEDURE DIVISION.\n",
111            "           MOVE 1 TO WS-ID.\n",
112            "           EXEC SQL OPEN C1 END-EXEC.\n",
113            "           PERFORM UNTIL SQLCODE NOT = 0\n",
114            "               EXEC SQL FETCH C1 INTO :WS-NAME, :WS-AMT:WS-IND\n",
115            "               END-EXEC\n",
116            "               IF SQLCODE = 0\n",
117            "                   MOVE WS-AMT TO E-AMT\n",
118            "                   MOVE WS-IND TO E-NUM\n",
119            "                   DISPLAY WS-NAME E-AMT E-NUM\n",
120            "               END-IF\n",
121            "           END-PERFORM.\n",
122            "           EXEC SQL CLOSE C1 END-EXEC.\n",
123            "           EXEC SQL SELECT STAMP INTO :WS-STAMP FROM EMP\n",
124            "                    WHERE ID = 1 END-EXEC.\n",
125            "           DISPLAY WS-STAMP.\n",
126            "           EXEC SQL INSERT INTO EMP (ID, NAME) VALUES (1, 'DUP')\n",
127            "           END-EXEC.\n",
128            "           MOVE SQLCODE TO E-CODE.\n",
129            "           DISPLAY 'INSERT ' E-CODE ' ' SQLSTATE.\n",
130            "           EXEC SQL UPDATE EMP SET AMT = AMT + 1 WHERE ID = 99\n",
131            "           END-EXEC.\n",
132            "           MOVE SQLCODE TO E-CODE.\n",
133            "           DISPLAY 'UPDATE ' E-CODE.\n",
134            "           EXEC SQL SELECT COUNT(*) INTO :WS-COUNT FROM EMP\n",
135            "                    WHERE AMT < 200 WITH UR END-EXEC.\n",
136            "           MOVE WS-COUNT TO E-NUM.\n",
137            "           DISPLAY 'COUNT ' E-NUM.\n",
138            "           EXEC SQL SELECT IBMREQD INTO :WS-Y\n",
139            "                    FROM SYSIBM.SYSDUMMY1 END-EXEC.\n",
140            "           DISPLAY 'DUMMY ' WS-Y.\n",
141            "           EXEC SQL SELECT NAME INTO :WS-NAME FROM EMP END-EXEC.\n",
142            "           MOVE SQLCODE TO E-CODE.\n",
143            "           DISPLAY 'MANY ' E-CODE.\n",
144            "           GOBACK.\n",
145        );
146
147        const EXPECTED: &str = concat!(
148            "ADAMS      00100.50 000\n",
149            "BAKER      00100.50-001\n",
150            "CLARK      00030.00 000\n",
151            "2020-01-02-03.04.05.500000\n",
152            "INSERT -803 23505\n",
153            "UPDATE  100\n",
154            "COUNT  002\n",
155            "DUMMY Y\n",
156            "MANY -811\n",
157        );
158
159        struct Sink(Rc<RefCell<Vec<u8>>>);
160
161        impl std::io::Write for Sink {
162            fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
163                self.0.borrow_mut().extend_from_slice(b);
164                Ok(b.len())
165            }
166            fn flush(&mut self) -> std::io::Result<()> {
167                Ok(())
168            }
169        }
170
171        fn run(mut database: Box<dyn Database + '_>) -> Result<String, String> {
172            let compiled = crate::compile(syntax::parse(PROGRAM).expect("parses"), &[]).expect("compiles");
173            let (mut out, mut err) = (Vec::new(), Vec::new());
174            let ran = compiled.execute_with(crate::unit::Library::default(), crate::files::Dds::default(), None, crate::unit::Clock::System, Some(database.as_mut()), &mut out, &mut err);
175            ran.map(|_| String::from_utf8(out).expect("DISPLAY writes text")).map_err(|a| format!("{}: {}", a.code, a.message))
176        }
177
178        #[test]
179        fn a_recorded_postgresql_run_replays_identically() {
180            let Some(url) = url() else { return };
181            let mut setup = Postgres::connect(&url, None).expect("connects");
182            setup.load_script(SCHEMA).expect("the schema loads");
183            let postgres = Postgres::connect(&url, None).expect("connects");
184            let source = postgres.source().to_owned();
185            assert!(source.starts_with("PostgreSQL 14"), "{source}");
186            let recording = Rc::new(RefCell::new(Vec::new()));
187            let recorder = Recorder::new(Box::new(postgres), Box::new(Sink(recording.clone())), &source).expect("records");
188            assert_eq!(run(Box::new(recorder)).as_deref(), Ok(EXPECTED));
189            let text = String::from_utf8(recording.borrow().clone()).expect("a recording is text");
190            let replayed = run(Box::new(Replay::parse(&text, false).expect("the recording parses")));
191            assert_eq!(replayed.as_deref(), Ok(EXPECTED), "{text}");
192        }
193
194        /// Lines that put `text` in STMT, thirty characters at a time.
195        fn set(text: &str) -> String {
196            let mut lines = String::from("           MOVE SPACES TO STMT-TEXT.\n");
197            for (k, chunk) in text.as_bytes().chunks(30).enumerate() {
198                let chunk = std::str::from_utf8(chunk).expect("ASCII");
199                lines += &format!("           MOVE '{chunk}' TO STMT-TEXT({}:{}).\n", 30 * k + 1, chunk.len());
200            }
201            lines + &format!("           MOVE {} TO STMT-LEN.\n", text.len())
202        }
203
204        fn shown(sql: &str, label: &str) -> String {
205            format!("           EXEC SQL {sql} END-EXEC.\n           MOVE SQLCODE TO E-CODE.\n           DISPLAY '{label} ' E-CODE.\n")
206        }
207
208        fn dynamic_program() -> String {
209            [
210                concat!(
211                    "       IDENTIFICATION DIVISION.\n",
212                    "       PROGRAM-ID. PGDYN.\n",
213                    "       DATA DIVISION.\n",
214                    "       WORKING-STORAGE SECTION.\n",
215                    "           EXEC SQL INCLUDE SQLCA END-EXEC.\n",
216                    "           EXEC SQL INCLUDE SQLDA END-EXEC.\n",
217                    "       01 E-NUM    PIC -9(4).\n",
218                    "       01 STMT.\n",
219                    "          49 STMT-LEN  PIC S9(4) COMP.\n",
220                    "          49 STMT-TEXT PIC X(120).\n",
221                    "       01 WS-ID    PIC S9(9) COMP.\n",
222                    "       01 WS-NAME  PIC X(10).\n",
223                    "       01 E-CODE   PIC -9(3).\n",
224                    "           EXEC SQL DECLARE C2 CURSOR FOR SEL END-EXEC.\n",
225                    "       PROCEDURE DIVISION.\n",
226                ),
227                &set("CREATE TABLE dyn (id integer PRIMARY KEY, name char(10))"),
228                &shown("EXECUTE IMMEDIATE :STMT", "CREATE"),
229                &set("INSERT INTO dyn VALUES (?, ?)"),
230                &shown("PREPARE INS FROM :STMT", "PREPARE"),
231                "           MOVE 1 TO WS-ID. MOVE 'ADAMS' TO WS-NAME.\n",
232                &shown("EXECUTE INS USING :WS-ID, :WS-NAME", "INSERT"),
233                "           MOVE 2 TO WS-ID. MOVE 'BAKER' TO WS-NAME.\n",
234                &shown("EXECUTE INS USING :WS-ID, :WS-NAME", "INSERT"),
235                &shown("EXECUTE INS USING :WS-ID, :WS-NAME", "DUPLICATE"),
236                &set("SELEC name FROM dyn"),
237                &shown("PREPARE BAD FROM :STMT", "SYNTAX"),
238                &set("SELECT name FROM nosuch"),
239                &shown("PREPARE BAD FROM :STMT", "NO TABLE"),
240                &set("SELECT name FROM dyn WHERE id >= ? ORDER BY id"),
241                &shown("PREPARE SEL FROM :STMT", "PREPARE"),
242                "           MOVE 1 TO WS-ID.\n",
243                &shown("OPEN C2 USING :WS-ID", "OPEN"),
244                concat!(
245                    "           PERFORM UNTIL SQLCODE NOT = 0\n",
246                    "               EXEC SQL FETCH C2 INTO :WS-NAME END-EXEC\n",
247                    "               IF SQLCODE = 0\n",
248                    "                   DISPLAY 'ROW ' WS-NAME\n",
249                    "               END-IF\n",
250                    "           END-PERFORM.\n",
251                ),
252                &shown("CLOSE C2", "CLOSE"),
253                "           MOVE 2 TO SQLN.\n",
254                &set("SELECT id, name FROM dyn"),
255                &shown("PREPARE S3 INTO :SQLDA FROM :STMT", "DESCRIBE"),
256                concat!(
257                    "           MOVE SQLD TO E-NUM.\n",
258                    "           DISPLAY 'SQLD ' E-NUM.\n",
259                    "           MOVE SQLTYPE(1) TO E-NUM.\n",
260                    "           DISPLAY 'ID ' E-NUM ' ' SQLNAMEC(1)(1:SQLNAMEL(1)).\n",
261                    "           MOVE SQLTYPE(2) TO E-NUM.\n",
262                    "           DISPLAY 'NAME ' E-NUM ' ' SQLLEN(2) ' '\n",
263                    "                   SQLNAMEC(2)(1:SQLNAMEL(2)).\n",
264                ),
265                &set("DROP TABLE dyn"),
266                &shown("EXECUTE IMMEDIATE :STMT", "DROP"),
267                "           GOBACK.\n",
268            ]
269            .concat()
270        }
271
272        const DYNAMIC_EXPECTED: &str = concat!(
273            "CREATE  000\n",
274            "PREPARE  000\n",
275            "INSERT  000\n",
276            "INSERT  000\n",
277            "DUPLICATE -803\n",
278            "SYNTAX -104\n",
279            "NO TABLE -204\n",
280            "PREPARE  000\n",
281            "OPEN  000\n",
282            "ROW ADAMS     \n",
283            "ROW BAKER     \n",
284            "CLOSE  000\n",
285            "DESCRIBE  000\n",
286            "SQLD  0002\n",
287            "ID  0496 ID\n",
288            "NAME  0453 0010 NAME\n",
289            "DROP  000\n",
290        );
291
292        fn run_dynamic(mut database: Box<dyn Database + '_>) -> Result<String, String> {
293            let compiled = crate::compile(syntax::parse(&dynamic_program()).expect("parses"), &[]).expect("compiles");
294            let (mut out, mut err) = (Vec::new(), Vec::new());
295            let ran = compiled.execute_with(crate::unit::Library::default(), crate::files::Dds::default(), None, crate::unit::Clock::System, Some(database.as_mut()), &mut out, &mut err);
296            ran.map(|_| String::from_utf8(out).expect("DISPLAY writes text")).map_err(|a| format!("{}: {}", a.code, a.message))
297        }
298
299        #[test]
300        fn dynamic_statements_run_against_postgresql_and_replay_identically() {
301            let Some(url) = url() else { return };
302            let mut setup = Postgres::connect(&url, None).expect("connects");
303            setup.load_script("DROP TABLE IF EXISTS dyn").expect("the table is dropped");
304            let postgres = Postgres::connect(&url, None).expect("connects");
305            let source = postgres.source().to_owned();
306            let recording = Rc::new(RefCell::new(Vec::new()));
307            let recorder = Recorder::new(Box::new(postgres), Box::new(Sink(recording.clone())), &source).expect("records");
308            assert_eq!(run_dynamic(Box::new(recorder)).as_deref(), Ok(DYNAMIC_EXPECTED));
309            let text = String::from_utf8(recording.borrow().clone()).expect("a recording is text");
310            assert!(text.contains(" PREPARE INS\n") && text.contains(" CREATE\n"), "{text}");
311            let replayed = run_dynamic(Box::new(Replay::parse(&text, false).expect("the recording parses")));
312            assert_eq!(replayed.as_deref(), Ok(DYNAMIC_EXPECTED), "{text}");
313        }
314
315        const ROWS_PROGRAM: &str = concat!(
316            "       IDENTIFICATION DIVISION.\n",
317            "       PROGRAM-ID. PGROWS.\n",
318            "       DATA DIVISION.\n",
319            "       WORKING-STORAGE SECTION.\n",
320            "           EXEC SQL INCLUDE SQLCA END-EXEC.\n",
321            "       01 IDS.\n",
322            "          05 R-ID   PIC S9(9) COMP OCCURS 4.\n",
323            "       01 NAMES.\n",
324            "          05 R-NAME PIC X(3) OCCURS 4.\n",
325            "       01 OUT-IDS.\n",
326            "          05 O-ID   PIC 9 OCCURS 3.\n",
327            "       01 OUT-NAMES.\n",
328            "          05 O-NAME PIC X(3) OCCURS 3.\n",
329            "       01 E-CODE   PIC -9(3).\n",
330            "       01 E-ROWS   PIC 9.\n",
331            "           EXEC SQL DECLARE C3 CURSOR WITH ROWSET POSITIONING FOR\n",
332            "                SELECT ID, NAME FROM RWS ORDER BY ID\n",
333            "           END-EXEC.\n",
334            "       PROCEDURE DIVISION.\n",
335            "           MOVE ZEROS TO OUT-IDS. MOVE SPACES TO OUT-NAMES.\n",
336            "           MOVE 1 TO R-ID(1). MOVE 2 TO R-ID(2).\n",
337            "           MOVE 3 TO R-ID(3). MOVE 1 TO R-ID(4).\n",
338            "           MOVE 'AAA' TO R-NAME(1). MOVE 'BBB' TO R-NAME(2).\n",
339            "           MOVE 'CCC' TO R-NAME(3). MOVE 'DUP' TO R-NAME(4).\n",
340            "           EXEC SQL INSERT INTO RWS (ID, NAME)\n",
341            "                VALUES (:R-ID, :R-NAME) FOR 4 ROWS\n",
342            "           END-EXEC.\n",
343            "           PERFORM SHOW-CODE.\n",
344            "           EXEC SQL INSERT INTO RWS (ID, NAME)\n",
345            "                VALUES (:R-ID, :R-NAME) FOR 4 ROWS\n",
346            "                NOT ATOMIC CONTINUE ON SQLEXCEPTION\n",
347            "           END-EXEC.\n",
348            "           PERFORM SHOW-CODE.\n",
349            "           EXEC SQL OPEN C3 END-EXEC.\n",
350            "           EXEC SQL FETCH NEXT ROWSET FROM C3 FOR 2 ROWS\n",
351            "                INTO :O-ID, :O-NAME\n",
352            "           END-EXEC.\n",
353            "           PERFORM SHOW-CODE.\n",
354            "           EXEC SQL FETCH NEXT ROWSET FROM C3\n",
355            "                INTO :O-ID, :O-NAME\n",
356            "           END-EXEC.\n",
357            "           PERFORM SHOW-CODE.\n",
358            "           EXEC SQL CLOSE C3 END-EXEC.\n",
359            "           GOBACK.\n",
360            "       SHOW-CODE.\n",
361            "           MOVE SQLCODE TO E-CODE.\n",
362            "           MOVE SQLERRD(3) TO E-ROWS.\n",
363            "           DISPLAY E-CODE ' ' E-ROWS ' ' O-ID(1) O-NAME(1)\n",
364            "                   O-ID(2) O-NAME(2).\n",
365        );
366
367        fn run_rows(program: &str, mut database: Box<dyn Database + '_>) -> Result<String, String> {
368            let compiled = crate::compile(syntax::parse(program).expect("parses"), &[]).expect("compiles");
369            let (mut out, mut err) = (Vec::new(), Vec::new());
370            let ran = compiled.execute_with(crate::unit::Library::default(), crate::files::Dds::default(), None, crate::unit::Clock::System, Some(database.as_mut()), &mut out, &mut err);
371            ran.map(|_| String::from_utf8(out).expect("DISPLAY writes text")).map_err(|a| format!("{}: {}", a.code, a.message))
372        }
373
374        #[test]
375        fn multiple_row_inserts_and_rowsets_run_against_postgresql_and_replay_identically() {
376            let Some(url) = url() else { return };
377            let mut setup = Postgres::connect(&url, None).expect("connects");
378            setup.load_script("DROP TABLE IF EXISTS rws; CREATE TABLE rws (id integer PRIMARY KEY, name char(3))").expect("the table is made");
379            let postgres = Postgres::connect(&url, None).expect("connects");
380            let source = postgres.source().to_owned();
381            let recording = Rc::new(RefCell::new(Vec::new()));
382            let recorder = Recorder::new(Box::new(postgres), Box::new(Sink(recording.clone())), &source).expect("records");
383            let expected = "-803 0 0   0   \n-253 3 0   0   \n 000 2 1AAA2BBB\n 100 1 3CCC2BBB\n";
384            assert_eq!(run_rows(ROWS_PROGRAM, Box::new(recorder)).as_deref(), Ok(expected));
385            let text = String::from_utf8(recording.borrow().clone()).expect("a recording is text");
386            assert!(text.contains("> int:3 | char:\"CCC\"\n> int:1 | char:\"DUP\"\n"), "{text}");
387            let replayed = run_rows(ROWS_PROGRAM, Box::new(Replay::parse(&text, false).expect("the recording parses")));
388            assert_eq!(replayed.as_deref(), Ok(expected), "{text}");
389            let call = ROWS_PROGRAM.replace("           EXEC SQL OPEN C3 END-EXEC.\n", "           EXEC SQL CALL NOPROC (:E-ROWS) END-EXEC.\n");
390            let refused = run_rows(&call, Box::new(Postgres::connect(&url, None).expect("connects")));
391            assert!(refused.as_ref().is_err_and(|e| e.starts_with("EXEC: EXEC SQL CALL NOPROC was reached: this database runs no stored procedures")), "{refused:?}");
392        }
393    }
394}