1use plsql_core::{Diagnostic, OracleFeature, OracleVersion, Severity, Span, UnknownReason};
13
14use crate::OracleTargetVersion;
15
16pub const UNSUPPORTED_DIALECT_FEATURE_CODE: &str = "PARSE_UNSUPPORTED_DIALECT_FEATURE";
19
20#[must_use]
25pub fn earliest_supporting_version(feature: OracleFeature) -> OracleVersion {
26 match feature {
27 OracleFeature::SqlMacros | OracleFeature::PolymorphicTableFunctions => {
28 OracleVersion::Oracle21c
29 }
30 OracleFeature::SqlBoolean23ai
31 | OracleFeature::PlsqlVector23ai
32 | OracleFeature::JsonRelationalDuality23ai => OracleVersion::Oracle23ai,
33 OracleFeature::BinaryVector26ai
34 | OracleFeature::SparseVector26ai
35 | OracleFeature::VectorArithmetic26ai
36 | OracleFeature::PackageResettable26ai
37 | OracleFeature::MultilingualEngineCallSpecs => OracleVersion::Oracle26ai,
38 }
39}
40
41#[must_use]
43pub fn feature_label(feature: OracleFeature) -> &'static str {
44 match feature {
45 OracleFeature::SqlBoolean23ai => "SQL `BOOLEAN`",
46 OracleFeature::PlsqlVector23ai => "PL/SQL `VECTOR`",
47 OracleFeature::BinaryVector26ai => "`BINARY VECTOR`",
48 OracleFeature::SparseVector26ai => "`SPARSE VECTOR`",
49 OracleFeature::VectorArithmetic26ai => "vector arithmetic operators",
50 OracleFeature::PackageResettable26ai => "package `RESETTABLE` clause",
51 OracleFeature::JsonRelationalDuality23ai => "JSON relational duality",
52 OracleFeature::SqlMacros => "SQL macros",
53 OracleFeature::PolymorphicTableFunctions => "polymorphic table functions",
54 OracleFeature::MultilingualEngineCallSpecs => "multilingual engine call specs",
55 }
56}
57
58fn target_version_label(target: OracleTargetVersion) -> &'static str {
59 match target {
60 OracleTargetVersion::Oracle11g => "Oracle 11g",
61 OracleTargetVersion::Oracle12c => "Oracle 12c",
62 OracleTargetVersion::Oracle19c => "Oracle 19c",
63 OracleTargetVersion::Oracle21c => "Oracle 21c",
64 OracleTargetVersion::Oracle23ai => "Oracle 23ai",
65 OracleTargetVersion::Oracle26ai => "Oracle 26ai",
66 }
67}
68
69fn version_label(version: OracleVersion) -> &'static str {
70 match version {
71 OracleVersion::Oracle11g => "Oracle 11g",
72 OracleVersion::Oracle12c => "Oracle 12c",
73 OracleVersion::Oracle19c => "Oracle 19c",
74 OracleVersion::Oracle21c => "Oracle 21c",
75 OracleVersion::Oracle23ai => "Oracle 23ai",
76 OracleVersion::Oracle26ai => "Oracle 26ai",
77 }
78}
79
80fn target_supports_feature(target: OracleTargetVersion, feature: OracleFeature) -> bool {
81 let target_version = match target {
82 OracleTargetVersion::Oracle11g => OracleVersion::Oracle11g,
83 OracleTargetVersion::Oracle12c => OracleVersion::Oracle12c,
84 OracleTargetVersion::Oracle19c => OracleVersion::Oracle19c,
85 OracleTargetVersion::Oracle21c => OracleVersion::Oracle21c,
86 OracleTargetVersion::Oracle23ai => OracleVersion::Oracle23ai,
87 OracleTargetVersion::Oracle26ai => OracleVersion::Oracle26ai,
88 };
89 target_version.default_features().contains(&feature)
90}
91
92#[must_use]
102pub fn unsupported_dialect_feature_remediation(
103 feature: OracleFeature,
104 target: OracleTargetVersion,
105) -> String {
106 let label = feature_label(feature);
107 let earliest = earliest_supporting_version(feature);
108 let earliest_label = version_label(earliest);
109 let target_label = target_version_label(target);
110 let mut hint = format!(
111 "{label} is available in {earliest_label} or later, but the parse target is {target_label}. Either raise the `parse_options.oracle_version` to a version that supports it, or rewrite the source to avoid this construct."
112 );
113 if let Some(extra) = workaround_hint(feature) {
114 hint.push(' ');
115 hint.push_str(extra);
116 }
117 hint
118}
119
120fn workaround_hint(feature: OracleFeature) -> Option<&'static str> {
123 match feature {
124 OracleFeature::SqlBoolean23ai => Some(
125 "Workaround: model the column as `NUMBER(1)` with a `CHECK (col IN (0,1))` constraint.",
126 ),
127 OracleFeature::PlsqlVector23ai
128 | OracleFeature::BinaryVector26ai
129 | OracleFeature::SparseVector26ai
130 | OracleFeature::VectorArithmetic26ai => Some(
131 "Workaround: store the vector as a CLOB / BLOB and compute distances in PL/SQL until upgrade.",
132 ),
133 OracleFeature::PackageResettable26ai => Some(
134 "Workaround: avoid `RESETTABLE` on the package; reset state explicitly in an initialization routine.",
135 ),
136 OracleFeature::JsonRelationalDuality23ai => Some(
137 "Workaround: model the duality view as a regular view plus an INSTEAD OF trigger until upgrade.",
138 ),
139 OracleFeature::SqlMacros => Some(
140 "Workaround: expand the macro manually in callers, or wrap it in a function (with a perf cost).",
141 ),
142 OracleFeature::PolymorphicTableFunctions => Some(
143 "Workaround: write a per-shape table function until polymorphic table functions are available.",
144 ),
145 OracleFeature::MultilingualEngineCallSpecs => Some(
146 "Workaround: use the equivalent Java / external procedure call spec for the older target.",
147 ),
148 }
149}
150
151#[must_use]
157pub fn unsupported_dialect_feature_diagnostic(
158 feature: OracleFeature,
159 target: OracleTargetVersion,
160 span: Option<Span>,
161) -> Option<Diagnostic> {
162 if target_supports_feature(target, feature) {
163 return None;
164 }
165 let label = feature_label(feature);
166 let target_label = target_version_label(target);
167 let mut diagnostic = Diagnostic::new(
168 UNSUPPORTED_DIALECT_FEATURE_CODE,
169 Severity::Error,
170 format!("{label} is not supported when parsing against {target_label}"),
171 );
172 diagnostic.primary_span = span;
173 diagnostic.help = Some(unsupported_dialect_feature_remediation(feature, target));
174 diagnostic
175 .unknown_reasons
176 .push(UnknownReason::UnsupportedDialectFeature);
177 Some(diagnostic)
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use plsql_core::{FileId, Position};
184
185 #[test]
186 fn earliest_version_table_matches_oracle_version_defaults() {
187 assert_eq!(
189 earliest_supporting_version(OracleFeature::SqlBoolean23ai),
190 OracleVersion::Oracle23ai
191 );
192 assert_eq!(
193 earliest_supporting_version(OracleFeature::SqlMacros),
194 OracleVersion::Oracle21c
195 );
196 assert_eq!(
197 earliest_supporting_version(OracleFeature::BinaryVector26ai),
198 OracleVersion::Oracle26ai
199 );
200 }
201
202 #[test]
203 fn diagnostic_emitted_for_unsupported_feature_on_lower_target() {
204 let span = Some(Span::new(
205 FileId::new(0),
206 Position::new(1, 1, 0),
207 Position::new(1, 11, 10),
208 ));
209 let diagnostic = unsupported_dialect_feature_diagnostic(
210 OracleFeature::SqlBoolean23ai,
211 OracleTargetVersion::Oracle19c,
212 span,
213 )
214 .expect("expected diagnostic for 23ai feature on 19c target");
215
216 assert_eq!(diagnostic.code, UNSUPPORTED_DIALECT_FEATURE_CODE);
217 assert_eq!(diagnostic.severity, Severity::Error);
218 assert!(diagnostic.message.contains("SQL `BOOLEAN`"));
219 assert!(diagnostic.message.contains("Oracle 19c"));
220 let help = diagnostic.help.as_deref().expect("help");
221 assert!(help.contains("Oracle 23ai or later"));
222 assert!(help.contains("NUMBER(1)"));
223 assert_eq!(diagnostic.primary_span, span);
224 assert_eq!(diagnostic.unknown_reasons.len(), 1);
225 assert!(matches!(
226 diagnostic.unknown_reasons[0],
227 UnknownReason::UnsupportedDialectFeature
228 ));
229 }
230
231 #[test]
232 fn no_diagnostic_when_target_supports_feature() {
233 assert!(
234 unsupported_dialect_feature_diagnostic(
235 OracleFeature::SqlBoolean23ai,
236 OracleTargetVersion::Oracle23ai,
237 None,
238 )
239 .is_none()
240 );
241 }
242
243 #[test]
244 fn vector_workarounds_consolidate_under_one_hint() {
245 for feature in [
246 OracleFeature::PlsqlVector23ai,
247 OracleFeature::BinaryVector26ai,
248 OracleFeature::SparseVector26ai,
249 OracleFeature::VectorArithmetic26ai,
250 ] {
251 let hint =
252 unsupported_dialect_feature_remediation(feature, OracleTargetVersion::Oracle19c);
253 assert!(
254 hint.contains("CLOB"),
255 "feature {feature:?} hint missing workaround"
256 );
257 }
258 }
259
260 #[test]
261 fn remediation_lists_earliest_version_label() {
262 let hint = unsupported_dialect_feature_remediation(
263 OracleFeature::PackageResettable26ai,
264 OracleTargetVersion::Oracle23ai,
265 );
266 assert!(hint.contains("Oracle 26ai or later"));
267 assert!(hint.contains("Oracle 23ai"));
268 assert!(hint.contains("RESETTABLE"));
269 }
270
271 #[test]
272 fn all_features_have_workaround_hints() {
273 for feature in [
274 OracleFeature::SqlBoolean23ai,
275 OracleFeature::PlsqlVector23ai,
276 OracleFeature::BinaryVector26ai,
277 OracleFeature::SparseVector26ai,
278 OracleFeature::VectorArithmetic26ai,
279 OracleFeature::PackageResettable26ai,
280 OracleFeature::JsonRelationalDuality23ai,
281 OracleFeature::SqlMacros,
282 OracleFeature::PolymorphicTableFunctions,
283 OracleFeature::MultilingualEngineCallSpecs,
284 ] {
285 let hint =
286 unsupported_dialect_feature_remediation(feature, OracleTargetVersion::Oracle11g);
287 assert!(
288 hint.to_lowercase().contains("workaround"),
289 "feature {feature:?} should carry a workaround hint"
290 );
291 }
292 }
293
294 #[test]
295 fn earliest_version_is_consistent_with_core_default_features() {
296 let ordered = [
304 OracleVersion::Oracle11g,
305 OracleVersion::Oracle12c,
306 OracleVersion::Oracle19c,
307 OracleVersion::Oracle21c,
308 OracleVersion::Oracle23ai,
309 OracleVersion::Oracle26ai,
310 ];
311 let features = [
312 OracleFeature::SqlBoolean23ai,
313 OracleFeature::PlsqlVector23ai,
314 OracleFeature::BinaryVector26ai,
315 OracleFeature::SparseVector26ai,
316 OracleFeature::VectorArithmetic26ai,
317 OracleFeature::PackageResettable26ai,
318 OracleFeature::JsonRelationalDuality23ai,
319 OracleFeature::SqlMacros,
320 OracleFeature::PolymorphicTableFunctions,
321 OracleFeature::MultilingualEngineCallSpecs,
322 ];
323 let idx = |v: OracleVersion| ordered.iter().position(|x| *x == v).unwrap();
324
325 for f in features {
326 let earliest = earliest_supporting_version(f);
327 for &v in &ordered {
328 let expected = idx(v) >= idx(earliest);
329 assert_eq!(
330 v.default_features().contains(&f),
331 expected,
332 "{f:?}: default_features({v:?}).contains == {} but earliest is {earliest:?}",
333 !expected
334 );
335 }
336 }
337 }
338}