Skip to content

Commit 5c20be4

Browse files
committed
Extract procedure object for EXECUTE PROCEDURE, CALL and JDBC call escape
1 parent 72768e2 commit 5c20be4

3 files changed

Lines changed: 171 additions & 17 deletions

File tree

src/main/org/firebirdsql/jaybird/parser/LocalStatementType.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: Copyright 2021-2024 Mark Rotteveel
1+
// SPDX-FileCopyrightText: Copyright 2021-2026 Mark Rotteveel
22
// SPDX-License-Identifier: LGPL-2.1-or-later
33
package org.firebirdsql.jaybird.parser;
44

@@ -31,6 +31,40 @@ public enum LocalStatementType {
3131
* {@code EXECUTE PROCEDURE} statement.
3232
*/
3333
EXECUTE_PROCEDURE(LocalStatementClass.DML),
34+
/**
35+
* {@code CALL} statement.
36+
*
37+
* @since 7
38+
*/
39+
CALL(LocalStatementClass.DML),
40+
/**
41+
* JDBC call escape without return parameter (i.e. {@code {call procname ...}}).
42+
* <p>
43+
* Callable statement V2 only.
44+
* </p>
45+
*
46+
* @since 7
47+
*/
48+
JDBC_CALL_ESCAPE(LocalStatementClass.DML),
49+
/**
50+
* JDBC call escape with return parameter (i.e. {@code {?=call procname ...}}).
51+
* <p>
52+
* Callable statement V2 only.
53+
* </p>
54+
*
55+
* @since 7
56+
*/
57+
JDBC_CALL_RETURN_ESCAPE(LocalStatementClass.DML),
58+
/**
59+
* JDBC escape (probably CALL escape) after {@code USING ... DO}, we can't handle this the same as a bare call
60+
* escape.
61+
* <p>
62+
* Callable statement V2 only.
63+
* </p>
64+
*
65+
* @since 7
66+
*/
67+
JDBC_ESCAPE_AFTER_USING(LocalStatementClass.DML),
3468
/**
3569
* {@code UPDATE} statement (or {@code UPDATE OR INSERT} before detection is complete).
3670
*/

src/main/org/firebirdsql/jaybird/parser/StatementDetector.java

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,19 @@
1414
import static org.firebirdsql.jaybird.parser.CharSequenceComparison.caseInsensitiveComparator;
1515

1616
/**
17-
* Detects the type of statement, and - optionally - whether a DML statement has a {@code RETURNING} clause.
17+
* Detects the type of statement, and statement specific information like target object and {@code RETURNING} clause.
1818
* <p>
1919
* If the detected statement type is {@code UPDATE}, {@code DELETE}, {@code INSERT}, {@code UPDATE OR INSERT} and
2020
* {@code MERGE}, it identifies the affected table and - optionally - if a {@code RETURNING} clause is present
2121
* (delegated to a {@link ReturningClauseDetector}).
2222
* </p>
2323
* <p>
24-
* The types of statements detected are informed by the needs of Jaybird, and may change between point releases.
24+
* If the detected statement is {@code EXECUTE PROCEDURE}, {@code CALL}, or - Callable Statement V2 only - a JDBC call
25+
* escape, it identifies the stored procedure.
26+
* </p>
27+
* <p>
28+
* The types of statements detected and other information are informed by the needs of Jaybird, and may change between
29+
* point releases.
2530
* </p>
2631
*
2732
* @author Mark Rotteveel
@@ -56,6 +61,11 @@ public final class StatementDetector implements TokenVisitor {
5661
nextAfterStart.put("(", new StateAfterStart(ParserState.SELECT, LocalStatementType.SELECT));
5762
// Firebird 6.0+ USING ... DO; need to find end of the USING clause to detect actual statement type
5863
nextAfterStart.put("USING", new StateAfterStart(ParserState.FIND_USING_END, LocalStatementType.OTHER));
64+
// Firebird 6.0+ CALL
65+
nextAfterStart.put("CALL", new StateAfterStart(ParserState.CALL, LocalStatementType.CALL));
66+
// JDBC escape (search for call escape)
67+
// NOTE: JDBC call escape detection only applies to CallableStatement V2 handling
68+
nextAfterStart.put("{", new StateAfterStart(ParserState.JDBC_ESCAPE_START, LocalStatementType.OTHER));
5969
NEXT_AFTER_START = unmodifiableMap(nextAfterStart);
6070
}
6171

@@ -115,16 +125,19 @@ public void visitToken(Token token, VisitorRegistrar visitorRegistrar) {
115125
visitorRegistrar.removeVisitor(this);
116126
} else {
117127
switch (parserState) {
128+
case EXECUTE_PROCEDURE:
129+
case CALL:
130+
case JDBC_ESCAPE_CALL:
118131
case INSERT_INTO:
119132
case DML_TARGET:
120133
case DML_TARGET_FORWARD_TOKEN: {
121134
targetObjectExtractor = new ObjectReferenceExtractor();
122-
TokenVisitor registeredVisitor = targetObjectExtractor.onRemoveRegister(this);
123-
visitorRegistrar.addVisitor(registeredVisitor);
135+
TokenVisitor newVisitor = targetObjectExtractor.onRemoveRegister(this);
136+
visitorRegistrar.addVisitor(newVisitor);
124137
visitorRegistrar.removeVisitor(this);
125138
if (parserState == ParserState.DML_TARGET_FORWARD_TOKEN) {
126139
parserState = ParserState.DML_TARGET;
127-
registeredVisitor.visitToken(token, visitorRegistrar);
140+
newVisitor.visitToken(token, visitorRegistrar);
128141
}
129142
break;
130143
}
@@ -152,10 +165,11 @@ public void visitToken(Token token, VisitorRegistrar visitorRegistrar) {
152165
@Override
153166
public void complete(VisitorRegistrar visitorRegistrar) {
154167
switch (parserState) {
155-
case INSERT_INTO -> updateStatementType(LocalStatementType.OTHER);
156-
case DML_TARGET -> {
168+
// if parsing completes in these states, the statement is incomplete
169+
case JDBC_ESCAPE_CALL, INSERT_INTO -> updateStatementType(LocalStatementType.OTHER);
170+
// Handle DELETE FROM ... without WHERE, and EXECUTE PROCEDURE ... without arguments
171+
case EXECUTE_PROCEDURE, DML_TARGET -> {
157172
// TODO Maybe remove complete(..) and instead have the parser post an EOF token?
158-
// Handle DELETE FROM ... without WHERE
159173
if (targetObject == null && !trySetTargetObject()) {
160174
updateStatementType(LocalStatementType.OTHER);
161175
}
@@ -208,9 +222,6 @@ private enum ParserState {
208222
START {
209223
@Override
210224
ParserState next(Token token, StatementDetector detector) {
211-
if (!(token instanceof ReservedToken || token instanceof ParenthesisOpen)) {
212-
return forceOther(detector);
213-
}
214225
StateAfterStart stateAfterStart =
215226
NEXT_AFTER_START.getOrDefault(token.textAsCharSequence(), INITIAL_OTHER);
216227
detector.updateStatementType(stateAfterStart.type);
@@ -228,7 +239,80 @@ ParserState next(Token token, StatementDetector detector) {
228239
return forceOther(detector);
229240
}
230241
},
231-
EXECUTE_PROCEDURE(true),
242+
EXECUTE_PROCEDURE {
243+
@Override
244+
ParserState next(Token token, StatementDetector detector) {
245+
if (detector.trySetTargetObject()) {
246+
return EXEC_PROC_ARGS;
247+
}
248+
return forceOther(detector);
249+
}
250+
},
251+
// TODO Final state for now; maybe merge with CALL_PROC_ARGS and/or JDBC_CALL_PROC_ARGS
252+
// TODO Do we need to parse and extract the arguments?
253+
EXEC_PROC_ARGS(true),
254+
CALL {
255+
@Override
256+
ParserState next(Token token, StatementDetector detector) {
257+
if (detector.trySetTargetObject()) {
258+
return CALL_PROC_ARGS;
259+
}
260+
return forceOther(detector);
261+
}
262+
},
263+
// TODO Final state for now; maybe merge with EXEC_PROC_ARGS and/or JDBC_CALL_PROC_ARGS
264+
// TODO Do we need to parse and extract the arguments? Maybe for named support, or can we get that from prepare?
265+
CALL_PROC_ARGS(true),
266+
// NOTE: JDBC call escape detection only applies to CallableStatement V2 handling
267+
JDBC_ESCAPE_START {
268+
@Override
269+
ParserState next(Token token, StatementDetector detector) {
270+
if (token instanceof PositionalParameterToken) {
271+
return JDBC_ESCAPE_POSSIBLY_CALL_QM;
272+
} else if (token.equalsIgnoreCase("CALL")) {
273+
// Call is not a reserved token in Firebird 5.0 and older
274+
detector.updateStatementType(LocalStatementType.JDBC_CALL_ESCAPE);
275+
return JDBC_ESCAPE_CALL;
276+
}
277+
return forceOther(detector);
278+
}
279+
},
280+
// NOTE: JDBC call escape detection only applies to CallableStatement V2 handling
281+
JDBC_ESCAPE_POSSIBLY_CALL_QM {
282+
@Override
283+
ParserState next(Token token, StatementDetector detector) {
284+
if (token instanceof OperatorToken && token.equalsIgnoreCase("=")) {
285+
return JDBC_ESCAPE_POSSIBLY_CALL_EQ;
286+
}
287+
return forceOther(detector);
288+
}
289+
},
290+
// NOTE: JDBC call escape detection only applies to CallableStatement V2 handling
291+
JDBC_ESCAPE_POSSIBLY_CALL_EQ {
292+
@Override
293+
ParserState next(Token token, StatementDetector detector) {
294+
if (token.equalsIgnoreCase("CALL")) {
295+
// Call is not a reserved token in Firebird 5.0 and older
296+
detector.updateStatementType(LocalStatementType.JDBC_CALL_RETURN_ESCAPE);
297+
return JDBC_ESCAPE_CALL;
298+
}
299+
return forceOther(detector);
300+
}
301+
},
302+
// NOTE: JDBC call escape detection only applies to CallableStatement V2 handling
303+
JDBC_ESCAPE_CALL {
304+
@Override
305+
ParserState next(Token token, StatementDetector detector) {
306+
if (detector.trySetTargetObject()) {
307+
return JDBC_CALL_PROC_ARGS;
308+
}
309+
return forceOther(detector);
310+
}
311+
},
312+
// TODO Final state for now; maybe merge with CALL_PROC_ARGS and/or EXEC_PROC_ARGS
313+
// TODO We need to parse and extract the arguments
314+
// NOTE: JDBC call escape detection only applies to CallableStatement V2 handling
315+
JDBC_CALL_PROC_ARGS(true),
232316
UPDATE {
233317
@Override
234318
ParserState next(Token token, StatementDetector detector) {
@@ -362,6 +446,12 @@ ParserState next(Token token, StatementDetector detector) {
362446
@Override
363447
ParserState next(Token token, StatementDetector detector) {
364448
// This is invoked after the end of USING ... DO has been found
449+
if (token instanceof CurlyBraceOpen) {
450+
// This is probably a CALL escape, but the normal handling cannot apply as USING can change how
451+
// parameters are defined and handled
452+
detector.updateStatementType(LocalStatementType.JDBC_ESCAPE_AFTER_USING);
453+
return OTHER;
454+
}
365455
return START.next(token, detector);
366456
}
367457
},

src/test/org/firebirdsql/jaybird/parser/StatementDetectorTest.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,13 @@ static Stream<Arguments> detectionCases() {
6666

6767
// EXECUTE PROCEDURE
6868
detectReturning("execute procedure test 'value1', 'value2'",
69-
LocalStatementType.EXECUTE_PROCEDURE, false),
70-
noDetect("execute procedure test 'value1', 'value2'", LocalStatementType.EXECUTE_PROCEDURE, false),
71-
// Presence of execute procedure as first two keywords is sufficient
72-
detectReturning("execute procedure", LocalStatementType.EXECUTE_PROCEDURE, true),
69+
LocalStatementType.EXECUTE_PROCEDURE, ObjectReference.of("TEST"), false, false),
70+
noDetect("execute procedure test('value1', 'value2')",
71+
LocalStatementType.EXECUTE_PROCEDURE, ObjectReference.of("TEST"), false),
72+
detectReturning("execute procedure test",
73+
LocalStatementType.EXECUTE_PROCEDURE, ObjectReference.of("TEST"), false, true),
74+
noDetect("execute procedure \"some_schema\".\"test\"",
75+
LocalStatementType.EXECUTE_PROCEDURE, ObjectReference.of("some_schema", "test"), true),
7376

7477
// DML
7578
// insert
@@ -257,6 +260,33 @@ declare function sub_func(DO integer) returns integer
257260
do delete from "sometable" where x = do returning id""",
258261
LocalStatementType.DELETE, ObjectReference.of("sometable"), true, true),
259262

263+
// JDBC call escape
264+
// TODO Will need further refinement (e.g. whole statement will need to be consumed, at least until closing brace)
265+
noDetect("{call someproc}", LocalStatementType.JDBC_CALL_ESCAPE, ObjectReference.of("SOMEPROC"), true),
266+
noDetect("{call someproc", LocalStatementType.OTHER, true),
267+
noDetect("{call someproc(param1, param2)}",
268+
LocalStatementType.JDBC_CALL_ESCAPE, ObjectReference.of("SOMEPROC"), false),
269+
noDetect("{?=call someproc}",
270+
LocalStatementType.JDBC_CALL_RETURN_ESCAPE, ObjectReference.of("SOMEPROC"), true),
271+
noDetect("{? = call someproc}",
272+
LocalStatementType.JDBC_CALL_RETURN_ESCAPE, ObjectReference.of("SOMEPROC"), true),
273+
noDetect("{? = call someproc(?)}",
274+
LocalStatementType.JDBC_CALL_RETURN_ESCAPE, ObjectReference.of("SOMEPROC"), false),
275+
// TODO Should result in OTHER after refinement due to missing closing brace
276+
// NOTE: Missing closing brace
277+
noDetect("{call someproc(param1, param2)",
278+
LocalStatementType.JDBC_CALL_ESCAPE, ObjectReference.of("SOMEPROC"), false),
279+
280+
// Firebird 6+ CALL
281+
noDetect("call insert_customer('LECLERC', 'CHARLES', null, ?)",
282+
LocalStatementType.CALL, ObjectReference.of("INSERT_CUSTOMER"), false),
283+
noDetect("""
284+
call insert_customer(
285+
last_name => 'LECLERC',
286+
first_name => 'CHARLES',
287+
last_name => ?,
288+
id => ?)""", LocalStatementType.CALL, ObjectReference.of("INSERT_CUSTOMER"), false),
289+
260290
// invalid syntax
261291
detectReturning("update or invalid", LocalStatementType.OTHER, true),
262292
noDetect("update or invalid", LocalStatementType.OTHER, true),

0 commit comments

Comments
 (0)