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_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 declarations_with_no_sql_type() {
61        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"]);
62        assert!(got.iter().all(Result::is_err), "{got:?}");
63    }
64
65    /// Against a live server named by IRONWORK_PG_URL, which `tools/pg-test.sh` starts in a
66    /// container; without it the test passes without running.
67    mod live {
68        use super::*;
69        use std::cell::RefCell;
70        use std::rc::Rc;
71
72        fn url() -> Option<String> {
73            std::env::var("IRONWORK_PG_URL").ok()
74        }
75
76        const SCHEMA: &str = "DROP TABLE IF EXISTS emp; \
77            CREATE TABLE emp (id integer PRIMARY KEY, name char(10) NOT NULL, amt numeric(7,2), stamp timestamp); \
78            INSERT INTO emp VALUES (1, 'ADAMS', 100.50, '2020-01-02 03:04:05.5'), (2, 'BAKER', NULL, NULL), (3, 'CLARK', 30, NULL); \
79            CREATE SCHEMA IF NOT EXISTS sysibm; \
80            CREATE OR REPLACE VIEW sysibm.sysdummy1 AS SELECT 'Y'::char(1) AS ibmreqd;";
81
82        const PROGRAM: &str = concat!(
83            "       IDENTIFICATION DIVISION.\n",
84            "       PROGRAM-ID. PGQ5.\n",
85            "       DATA DIVISION.\n",
86            "       WORKING-STORAGE SECTION.\n",
87            "           EXEC SQL INCLUDE SQLCA END-EXEC.\n",
88            "       01 WS-ID    PIC S9(9) COMP VALUE 2.\n",
89            "       01 WS-NAME  PIC X(10).\n",
90            "       01 WS-AMT   PIC S9(5)V99 COMP-3.\n",
91            "       01 WS-IND   PIC S9(4) COMP.\n",
92            "       01 WS-STAMP PIC X(26).\n",
93            "       01 WS-Y     PIC X.\n",
94            "       01 WS-COUNT PIC S9(9) COMP.\n",
95            "       01 E-AMT    PIC -9(5).99.\n",
96            "       01 E-CODE   PIC -9(3).\n",
97            "       01 E-NUM    PIC -9(3).\n",
98            "           EXEC SQL DECLARE C1 CURSOR FOR\n",
99            "                SELECT NAME, AMT FROM EMP WHERE ID >= :WS-ID\n",
100            "                ORDER BY ID\n",
101            "           END-EXEC.\n",
102            "       PROCEDURE DIVISION.\n",
103            "           MOVE 1 TO WS-ID.\n",
104            "           EXEC SQL OPEN C1 END-EXEC.\n",
105            "           PERFORM UNTIL SQLCODE NOT = 0\n",
106            "               EXEC SQL FETCH C1 INTO :WS-NAME, :WS-AMT:WS-IND\n",
107            "               END-EXEC\n",
108            "               IF SQLCODE = 0\n",
109            "                   MOVE WS-AMT TO E-AMT\n",
110            "                   MOVE WS-IND TO E-NUM\n",
111            "                   DISPLAY WS-NAME E-AMT E-NUM\n",
112            "               END-IF\n",
113            "           END-PERFORM.\n",
114            "           EXEC SQL CLOSE C1 END-EXEC.\n",
115            "           EXEC SQL SELECT STAMP INTO :WS-STAMP FROM EMP\n",
116            "                    WHERE ID = 1 END-EXEC.\n",
117            "           DISPLAY WS-STAMP.\n",
118            "           EXEC SQL INSERT INTO EMP (ID, NAME) VALUES (1, 'DUP')\n",
119            "           END-EXEC.\n",
120            "           MOVE SQLCODE TO E-CODE.\n",
121            "           DISPLAY 'INSERT ' E-CODE ' ' SQLSTATE.\n",
122            "           EXEC SQL UPDATE EMP SET AMT = AMT + 1 WHERE ID = 99\n",
123            "           END-EXEC.\n",
124            "           MOVE SQLCODE TO E-CODE.\n",
125            "           DISPLAY 'UPDATE ' E-CODE.\n",
126            "           EXEC SQL SELECT COUNT(*) INTO :WS-COUNT FROM EMP\n",
127            "                    WHERE AMT < 200 WITH UR END-EXEC.\n",
128            "           MOVE WS-COUNT TO E-NUM.\n",
129            "           DISPLAY 'COUNT ' E-NUM.\n",
130            "           EXEC SQL SELECT IBMREQD INTO :WS-Y\n",
131            "                    FROM SYSIBM.SYSDUMMY1 END-EXEC.\n",
132            "           DISPLAY 'DUMMY ' WS-Y.\n",
133            "           EXEC SQL SELECT NAME INTO :WS-NAME FROM EMP END-EXEC.\n",
134            "           MOVE SQLCODE TO E-CODE.\n",
135            "           DISPLAY 'MANY ' E-CODE.\n",
136            "           GOBACK.\n",
137        );
138
139        const EXPECTED: &str = concat!(
140            "ADAMS      00100.50 000\n",
141            "BAKER      00100.50-001\n",
142            "CLARK      00030.00 000\n",
143            "2020-01-02-03.04.05.500000\n",
144            "INSERT -803 23505\n",
145            "UPDATE  100\n",
146            "COUNT  002\n",
147            "DUMMY Y\n",
148            "MANY -811\n",
149        );
150
151        struct Sink(Rc<RefCell<Vec<u8>>>);
152
153        impl std::io::Write for Sink {
154            fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
155                self.0.borrow_mut().extend_from_slice(b);
156                Ok(b.len())
157            }
158            fn flush(&mut self) -> std::io::Result<()> {
159                Ok(())
160            }
161        }
162
163        fn run(mut database: Box<dyn Database + '_>) -> Result<String, String> {
164            let compiled = crate::compile(syntax::parse(PROGRAM).expect("parses"), &[]).expect("compiles");
165            let (mut out, mut err) = (Vec::new(), Vec::new());
166            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);
167            ran.map(|_| String::from_utf8(out).expect("DISPLAY writes text")).map_err(|a| format!("{}: {}", a.code, a.message))
168        }
169
170        #[test]
171        fn a_recorded_postgresql_run_replays_identically() {
172            let Some(url) = url() else { return };
173            let mut setup = Postgres::connect(&url, None).expect("connects");
174            setup.load_script(SCHEMA).expect("the schema loads");
175            let postgres = Postgres::connect(&url, None).expect("connects");
176            let source = postgres.source().to_owned();
177            assert!(source.starts_with("PostgreSQL 14"), "{source}");
178            let recording = Rc::new(RefCell::new(Vec::new()));
179            let recorder = Recorder::new(Box::new(postgres), Box::new(Sink(recording.clone())), &source).expect("records");
180            assert_eq!(run(Box::new(recorder)).as_deref(), Ok(EXPECTED));
181            let text = String::from_utf8(recording.borrow().clone()).expect("a recording is text");
182            let replayed = run(Box::new(Replay::parse(&text, false).expect("the recording parses")));
183            assert_eq!(replayed.as_deref(), Ok(EXPECTED), "{text}");
184        }
185    }
186}