1pub 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 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 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}