@@ -75,49 +75,87 @@ impl WorkInstances {
7575 }
7676}
7777
78+ /// The only error that means "this release lacks the column"; anything else is
79+ /// a real failure and must not be retried away.
80+ fn is_unknown_column_error ( e : & anyhow:: Error ) -> bool {
81+ e. to_string ( ) . contains ( "ORA-00904" )
82+ }
83+
84+ /// Oracle changed the versioning information with release 18c, introducing
85+ /// `VERSION_FULL`. Where that is unavailable, `VERSION` carries the full
86+ /// versioning information instead.
87+ #[ derive( Debug ) ]
88+ enum DetectedVersion {
89+ Since18c ( InstanceVersion ) ,
90+ Before18c ( InstanceVersion ) ,
91+ }
92+
93+ impl DetectedVersion {
94+ fn version ( & self ) -> & InstanceVersion {
95+ match self {
96+ Self :: Since18c ( v) | Self :: Before18c ( v) => v,
97+ }
98+ }
99+ }
100+
101+ fn _detect_version ( spot : & OpenedSpot ) -> Result < DetectedVersion > {
102+ let ask = |sql : & str | -> Result < InstanceVersion > {
103+ let rows = spot. query_table ( & SqlQuery :: new ( sql, & Vec :: new ( ) ) ) . 0 ?;
104+ _extract_version ( rows. into_iter ( ) . map ( |row| row. join ( "" ) ) . collect ( ) )
105+ . map ( InstanceVersion :: from)
106+ . ok_or_else ( || anyhow:: anyhow!( "No usable version in v$instance" ) )
107+ } ;
108+ match ask ( sqls:: query:: internal:: INSTANCE_VERSION_FULL ) {
109+ Ok ( version) => Ok ( DetectedVersion :: Since18c ( version) ) ,
110+ Err ( e) if is_unknown_column_error ( & e) => {
111+ log:: info!( "No VERSION_FULL in v$instance ({e}), asking for VERSION instead" ) ;
112+ ask ( sqls:: query:: internal:: INSTANCE_VERSION ) . map ( DetectedVersion :: Before18c )
113+ }
114+ Err ( e) => Err ( e) ,
115+ }
116+ }
117+
78118fn _get_instances ( spot : & OpenedSpot , custom_query : Option < & str > ) -> Result < _InstanceEntries > {
79- let ( new_sql, old_sql) = if spot. target ( ) . is_asm ( ) {
80- (
81- sqls:: query:: internal:: ASM_INSTANCE_INFO_SQL_TEXT_NEW ,
82- sqls:: query:: internal:: ASM_INSTANCE_INFO_SQL_TEXT_OLD ,
83- )
84- } else {
85- (
86- sqls:: query:: internal:: INSTANCE_INFO_SQL_TEXT_NEW ,
87- sqls:: query:: internal:: INSTANCE_INFO_SQL_TEXT_OLD ,
88- )
119+ if let Some ( query) = custom_query {
120+ // Replaces the probe entirely; the caller owns the column set.
121+ return Ok ( _to_instance_entries (
122+ spot. query_table ( & SqlQuery :: new ( query, & Vec :: new ( ) ) ) . 0 ?,
123+ ) ) ;
124+ }
125+
126+ let detected = _detect_version ( spot) ?;
127+ log:: info!( "Instance reports {detected:?}" ) ;
128+
129+ let sql = match ( spot. target ( ) . is_asm ( ) , & detected) {
130+ ( true , DetectedVersion :: Since18c ( _) ) => {
131+ sqls:: query:: internal:: ASM_INSTANCE_INFO_SQL_TEXT_NEW
132+ }
133+ ( true , DetectedVersion :: Before18c ( _) ) => {
134+ sqls:: query:: internal:: ASM_INSTANCE_INFO_SQL_TEXT_OLD
135+ }
136+ ( false , DetectedVersion :: Since18c ( _) ) => sqls:: query:: internal:: INSTANCE_INFO_SQL_TEXT_NEW ,
137+ ( false , DetectedVersion :: Before18c ( _) ) => sqls:: query:: internal:: INSTANCE_INFO_SQL_TEXT_OLD ,
89138 } ;
90- if let Ok ( result) = spot
91- . query_table ( & SqlQuery :: new ( custom_query. unwrap_or ( new_sql) , & Vec :: new ( ) ) )
92- . 0
93- {
94- Ok ( _to_instance_entries ( result) )
95- } else {
96- let mut result = spot. query_table ( & SqlQuery :: new ( old_sql, & Vec :: new ( ) ) ) . 0 ?;
97- let result_with_version = spot
98- . query_table ( & SqlQuery :: new (
99- sqls:: query:: internal:: INSTANCE_APPROXIMATE_VERSION ,
100- & Vec :: new ( ) ,
101- ) )
102- . format ( "" ) ?;
103- if let Some ( version) = _extract_version ( result_with_version) {
104- log:: info!( "Extracted version: {version}" ) ;
105- for r in result. iter_mut ( ) {
106- r[ 2 ] = version. clone ( ) ; // Update the version column
107- }
139+
140+ let mut result = spot. query_table ( & SqlQuery :: new ( sql, & Vec :: new ( ) ) ) . 0 ?;
141+ for r in result. iter_mut ( ) {
142+ if let Some ( version_column) = r. get_mut ( 2 ) {
143+ * version_column = detected. version ( ) . clone ( ) . into ( ) ;
108144 }
109- Ok ( _to_instance_entries ( result) )
110145 }
146+ Ok ( _to_instance_entries ( result) )
111147}
112148
113149fn _extract_version ( result : Vec < String > ) -> Option < String > {
114150 if result. is_empty ( ) {
115151 log:: warn!( "No version information found in v$instance" ) ;
116152 return None ;
117153 }
118- result[ 0 ] . split ( ' ' ) . next_back ( ) . and_then ( |s| {
119- convert_to_num_version ( & InstanceVersion :: from ( s. to_owned ( ) ) ) . map ( |_| s. to_string ( ) )
120- } )
154+ result
155+ . iter ( )
156+ . flat_map ( |line| line. split_whitespace ( ) )
157+ . find ( |token| convert_to_num_version ( & InstanceVersion :: from ( ( * token) . to_owned ( ) ) ) . is_some ( ) )
158+ . map ( str:: to_string)
121159}
122160
123161fn _to_instance_entries ( result : Vec < Vec < String > > ) -> _InstanceEntries {
@@ -160,18 +198,115 @@ pub fn convert_to_num_version(version: &InstanceVersion) -> Option<InstanceNumVe
160198mod tests {
161199 use super :: * ;
162200 use crate :: config:: ora_sql:: Endpoint ;
163- use crate :: ora_sql:: backend:: test_support:: { instance_row , MiniOra } ;
201+ use crate :: ora_sql:: backend:: test_support:: MiniOra ;
164202 use crate :: ora_sql:: backend:: SpotBuilder ;
165203
204+ /// Returns the derived (version, tenant) plus every query the run issued.
205+ fn discover_at ( version : & str , cdb : & str ) -> ( InstanceNumVersion , Tenant , Vec < String > ) {
206+ let db = MiniOra :: at_version ( "ORCL" , version, cdb) ;
207+ let asked = std:: sync:: Arc :: clone ( & db. asked ) ;
208+ let spot = SpotBuilder :: new ( )
209+ . endpoint_target ( & Endpoint :: default ( ) )
210+ . custom_engine ( Box :: new ( db) )
211+ . build ( )
212+ . unwrap ( )
213+ . connect ( None )
214+ . unwrap ( ) ;
215+ let works = WorkInstances :: new ( & spot, None ) . expect ( "instance must be discovered" ) ;
216+ let ( v, t) = works
217+ . get_info ( & InstanceName :: from ( "ORCL" ) )
218+ . expect ( "ORCL must be present" ) ;
219+ let queries = asked. lock ( ) . unwrap ( ) . clone ( ) ;
220+ ( v, t, queries)
221+ }
222+
166223 #[ test]
167- fn test_get_version ( ) {
168- let simulated_spot = SpotBuilder :: new ( )
224+ fn test_18c_and_later_use_the_precise_version_and_ask_for_cdb ( ) {
225+ let ( version, tenant, asked) = discover_at ( "19.28.0.0.0" , "YES" ) ;
226+ assert_eq ! ( version, InstanceNumVersion :: from( 19_28_00_00 ) ) ;
227+ assert_eq ! ( tenant, Tenant :: Cdb ) ;
228+ assert ! (
229+ asked. iter( ) . any( |q| q. contains( "VERSION_FULL" ) ) ,
230+ "{asked:?}"
231+ ) ;
232+ assert ! ( asked. iter( ) . any( |q| q. contains( "d.cdb" ) ) , "{asked:?}" ) ;
233+ }
234+
235+ #[ test]
236+ fn test_12c_falls_back_to_version_but_still_asks_for_cdb ( ) {
237+ // No VERSION_FULL, but CON_ID and CDB are there: the probe degrades
238+ // while the instance query still reads the real tenancy.
239+ let ( version, tenant, asked) = discover_at ( "12.2.0.1.0" , "YES" ) ;
240+ assert_eq ! ( version, InstanceNumVersion :: from( 12_02_00_01 ) ) ;
241+ assert_eq ! ( tenant, Tenant :: Cdb ) ;
242+ assert ! (
243+ asked
244+ . iter( )
245+ . any( |q| q. trim( ) == "SELECT VERSION FROM v$instance" ) ,
246+ "the probe must fall back to VERSION: {asked:?}"
247+ ) ;
248+ assert ! ( asked. iter( ) . any( |q| q. contains( "d.cdb" ) ) , "{asked:?}" ) ;
249+ }
250+
251+ /// The supported floor is 12.1.0.2, so a release without `V$DATABASE.CDB`
252+ /// fails rather than being accommodated. Its version reaches the log first,
253+ /// which is what tells an operator why.
254+ #[ test]
255+ fn test_before_12c_fails_on_the_absent_cdb_column ( ) {
256+ let db = MiniOra :: at_version ( "ORCL" , "11.2.0.4.0" , "irrelevant" ) ;
257+ let spot = SpotBuilder :: new ( )
258+ . endpoint_target ( & Endpoint :: default ( ) )
259+ . custom_engine ( Box :: new ( db) )
260+ . build ( )
261+ . unwrap ( )
262+ . connect ( None )
263+ . unwrap ( ) ;
264+ let err = WorkInstances :: new ( & spot, None ) . expect_err ( "must not be accommodated" ) ;
265+ assert ! ( err. to_string( ) . contains( "ORA-00904" ) , "{err}" ) ;
266+ }
267+
268+ #[ test]
269+ fn test_a_real_error_is_not_treated_as_a_missing_column ( ) {
270+ // Anything but ORA-00904 must surface, or a permissions problem reads
271+ // as an ancient Oracle.
272+ let spot = SpotBuilder :: new ( )
169273 . endpoint_target ( & Endpoint :: default ( ) )
170274 . custom_engine ( Box :: new ( MiniOra {
171- instance_rows : vec ! [ instance_row ( "free" , "22.1.1.6.0" , "YES" ) ] ,
275+ absent_columns : vec ! [ "v$instance" . to_string ( ) ] ,
172276 ..Default :: default ( )
173277 } ) )
174278 . build ( )
279+ . unwrap ( )
280+ . connect ( None )
281+ . unwrap ( ) ;
282+ let err = WorkInstances :: new ( & spot, None ) . expect_err ( "must not be swallowed" ) ;
283+ assert ! ( err. to_string( ) . contains( "ORA-00904" ) , "{err}" ) ;
284+ }
285+
286+ #[ test]
287+ fn test_extract_version_is_not_positional ( ) {
288+ assert_eq ! (
289+ _extract_version( vec![ "23.26.0.24.03" . to_string( ) ] ) . as_deref( ) ,
290+ Some ( "23.26.0.24.03" )
291+ ) ;
292+ assert_eq ! (
293+ _extract_version( vec![
294+ "Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production"
295+ . to_string( )
296+ ] )
297+ . as_deref( ) ,
298+ Some ( "12.1.0.2.0" )
299+ ) ;
300+ assert_eq ! ( _extract_version( vec![ ] ) , None ) ;
301+ assert_eq ! ( _extract_version( vec![ "no version here" . to_string( ) ] ) , None ) ;
302+ }
303+
304+ #[ test]
305+ fn test_get_version ( ) {
306+ let simulated_spot = SpotBuilder :: new ( )
307+ . endpoint_target ( & Endpoint :: default ( ) )
308+ . custom_engine ( Box :: new ( MiniOra :: at_version ( "free" , "22.1.1.6.0" , "YES" ) ) )
309+ . build ( )
175310 . unwrap ( ) ;
176311 let conn = simulated_spot. connect ( None ) . unwrap ( ) ;
177312 assert_eq ! (
0 commit comments