@@ -17,6 +17,14 @@ import {
1717} from '../../utils/format.js' ;
1818import { bootSchemaStack } from '../../utils/schema-migrate.js' ;
1919
20+ /** The table this pre-flight inventories. Never written. */
21+ const SYS_ACCOUNT = 'sys_account' ;
22+
23+ /** The driver read this pre-flight issues: `find` only, read-only by construction. */
24+ interface AccountTableDriver {
25+ find ( object : string , query : Record < string , unknown > ) : Promise < unknown > ;
26+ }
27+
2028/**
2129 * `os migrate account-issuer` — the PLAN leg of the `sys_account.issuer`
2230 * retirement (#17440).
@@ -126,26 +134,45 @@ export default class MigrateAccountIssuer extends Command {
126134 const engine = ( stack . kernel as { getService ?: ( n : string ) => unknown } ) . getService ?. call (
127135 stack . kernel ,
128136 'objectql' ,
129- ) ;
137+ ) as { getDriverForObject ?: ( object : string ) => AccountTableDriver | undefined } | undefined ;
130138
131139 if ( ! flags . json ) printStep ( 'Scanning sys_account…' ) ;
132140
141+ // The pre-flight reads the PHYSICAL `sys_account` table through the
142+ // driver the engine routes that name to, not through the engine's
143+ // `find`. This boot composes no `AuthPlugin`, so `sys_account` is not a
144+ // registered object here, and the engine refuses a name its registry
145+ // does not resolve (`OBJECT_NOT_FOUND`) before any driver is asked. That
146+ // refusal is a fact about this boot's composition, never about the
147+ // database: ⛔ reading it as "no rows" would report a table full of
148+ // accounts as a clean pre-flight and authorise the drop. The table is
149+ // read in its legacy shape, `issuer` included, which is the very column
150+ // the registered schema no longer declares, so the driver (the path the
151+ // engine leaves to host code) is the reader this inventory needs.
152+ const driver = engine ?. getDriverForObject ?.( SYS_ACCOUNT ) ;
153+ if ( ! driver || typeof driver . find !== 'function' ) {
154+ throw new Error (
155+ `No driver serves ${ SYS_ACCOUNT } on this stack, so the table cannot be enumerated. ` +
156+ 'Refusing rather than reporting an unread table as clean.' ,
157+ ) ;
158+ }
159+
133160 // [#21552] A database with no `sys_account` table holds no account, so no
134161 // two rows collide: the probe reads it as no rows. The read is not
135- // avoided, measured: this boot composes no `AuthPlugin`, so `sys_account`
136- // is not a registered object and the held-back sync never lists it, and
162+ // avoided, measured: `sys_account` is not a registered object on this
163+ // boot, so the held-back sync never lists it, and
137164 // `stack.tableAbsent('sys_account')` answers false on every database.
138- // The refusal is therefore recognised, with the shared predicate and for
139- // this command's own table only. ⛔ No other refused read is softened: it
140- // still throws the probe's refusal below and is never read as clean.
165+ // The driver's missing-table refusal is therefore recognised, with the
166+ // shared predicate and for this command's own table only. ⛔ No other
167+ // refused read is softened: it still throws the probe's refusal below
168+ // and is never read as clean.
141169 let noAccountTable = false ;
142- const readEngine = engine as Parameters < typeof probeAccountIdentityCollisions > [ 0 ] ;
143- const readView : typeof readEngine = {
144- find : async ( object , query , options ) => {
170+ const readView : Parameters < typeof probeAccountIdentityCollisions > [ 0 ] = {
171+ find : async ( object , query ) => {
145172 try {
146- return await readEngine . find ( object , query , options ) ;
173+ return await driver . find ( object , query ) ;
147174 } catch ( error ) {
148- if ( object !== 'sys_account' || ! isMissingTableError ( error , object ) ) throw error ;
175+ if ( object !== SYS_ACCOUNT || ! isMissingTableError ( error , object ) ) throw error ;
149176 noAccountTable = true ;
150177 return [ ] ;
151178 }
0 commit comments