@@ -425,7 +425,7 @@ export default class Serve extends Command {
425425 // "missing artifact" error and assemble a bare kernel that
426426 // can later install marketplace apps at runtime.
427427 const { createDefaultHostConfig } = await import ( '@objectstack/runtime' ) ;
428- const bootResult = await createDefaultHostConfig ( { requireArtifact : ! useEmptyBoot } ) ;
428+ const bootResult = await createDefaultHostConfig ( { requireArtifact : ! useEmptyBoot , dev : isDev } ) ;
429429 config = { ...originalConfig , ...bootResult } as any ;
430430 } else if ( resolvedMode === 'standalone' ) {
431431 const { createStandaloneStack } = await import ( '@objectstack/runtime' ) ;
@@ -435,6 +435,9 @@ export default class Serve extends Command {
435435 const standaloneInput = {
436436 ...( config . standalone ?? { } ) ,
437437 projectRoot : ( config . standalone ?. projectRoot ?? path . dirname ( absolutePath ) ) ,
438+ // #2229: dev enables the native-better-sqlite3 → wasm → in-memory
439+ // step-down in the shared datasource factory; prod fails loudly.
440+ dev : isDev ,
438441 } ;
439442 const bootResult = await createStandaloneStack ( standaloneInput ) ;
440443 config = { ...originalConfig , ...bootResult } as any ;
@@ -630,19 +633,25 @@ export default class Serve extends Command {
630633 resolvedDriverLabel = 'MongoDBDriver' ;
631634 resolvedDatabaseUrl = databaseUrl ?? 'mongodb://localhost:27017/objectstack' ;
632635 } else if ( driverType === 'sqlite' || driverType === 'sql' ) {
633- const { SqlDriver } = await import ( '@objectstack/driver-sql' ) ;
634636 const filePath = ( databaseUrl ?? ':memory:' ) . replace ( / ^ f i l e : / , '' ) . replace ( / ^ s q l i t e : / , '' ) . replace ( / ^ s q l : \/ \/ / , '' ) ;
635- await kernel . use ( new DriverPlugin ( new SqlDriver ( {
636- client : 'better-sqlite3' ,
637- connection : { filename : filePath } ,
638- useNullAsDefault : true ,
637+ // Probe-by-connect with a dev-only native → wasm → in-memory
638+ // step-down (#2229). better-sqlite3 loads its native addon lazily
639+ // (first query), so an ABI mismatch is invisible here and would
640+ // otherwise surface much later as a runtime crash. resolveSqliteDriver
641+ // forces the load and degrades gracefully in dev / fails loudly in prod.
642+ const { resolveSqliteDriver } = await import ( '@objectstack/service-datasource' ) ;
643+ const resolved = await resolveSqliteDriver ( {
644+ filename : filePath ,
645+ dev : isDev ,
639646 // #2186: in dev, self-heal a persisted DB when a metadata change
640647 // relaxes a constraint (loosen-only; never destructive / never in prod).
641648 autoMigrate : isDev ? 'safe' : undefined ,
642- } ) as any ) ) ;
643- trackPlugin ( 'SqlDriver' ) ;
644- resolvedDriverLabel = 'SqlDriver(sqlite)' ;
645- resolvedDatabaseUrl = databaseUrl ?? ':memory:' ;
649+ warn : ( m ) => console . warn ( chalk . yellow ( m ) ) ,
650+ } ) ;
651+ await kernel . use ( new DriverPlugin ( resolved . driver ) ) ;
652+ trackPlugin ( resolved . engine === 'memory' ? 'MemoryDriver' : resolved . engine === 'sqlite-wasm' ? 'SqliteWasmDriver' : 'SqlDriver' ) ;
653+ resolvedDriverLabel = resolved . label ;
654+ resolvedDatabaseUrl = resolved . engine === 'memory' ? '(in-memory)' : ( databaseUrl ?? ':memory:' ) ;
646655 } else if ( driverType === 'sqlite-wasm' || driverType === 'wasm-sqlite' || driverType === 'wasm' ) {
647656 const { SqliteWasmDriver } = await import ( '@objectstack/driver-sqlite-wasm' ) ;
648657 const filePath = ( databaseUrl ?? ':memory:' ) . replace ( / ^ f i l e : / , '' ) . replace ( / ^ w a s m - s q l i t e : \/ \/ / , '' ) . replace ( / ^ s q l i t e : / , '' ) ;
@@ -676,90 +685,26 @@ export default class Serve extends Command {
676685 resolvedDriverLabel = 'SqlDriver(mysql2)' ;
677686 resolvedDatabaseUrl = databaseUrl ;
678687 } else if ( isDev ) {
679- // Default in dev: prefer native SQLite for production-like SQL
680- // semantics at native speed. When the native `better-sqlite3`
681- // binary is unavailable — not built, ABI mismatch after a Node
682- // upgrade (e.g. Node 25 → NODE_MODULE_VERSION mismatch), or a
683- // blocked prebuild download — fall back to the pure-JS wasm SQLite
684- // driver, which keeps *real* SQL semantics (and on-disk
685- // persistence) without any native build step. Only if wasm also
686- // fails to load do we drop to the in-memory driver (mingo), which
687- // is neither real SQL nor persistent.
688- //
689- // knex loads its client lazily (at first query, not at construction),
690- // so the only reliable signal inside this registration window is to
691- // actually open a connection: connect() runs `SELECT 1`, which forces
692- // better-sqlite3 to load. If that throws we step down the chain here
693- // instead of letting the failure surface much later — as a
694- // missing-module crash on the first real query — or be swallowed by
695- // the silent catch below, leaving the kernel with no driver at all.
696- let sqliteDriver : any ;
697- let sqliteOk = false ;
698- try {
699- const { SqlDriver } = await import ( '@objectstack/driver-sql' ) ;
700- sqliteDriver = new SqlDriver ( {
701- client : 'better-sqlite3' ,
702- connection : { filename : ':memory:' } ,
703- useNullAsDefault : true ,
704- autoMigrate : 'safe' , // #2186 dev loosen-only self-heal
705- } ) ;
706- await sqliteDriver . connect ( ) ;
707- sqliteOk = true ;
708- } catch {
709- sqliteOk = false ;
710- if ( sqliteDriver ?. disconnect ) {
711- try { await sqliteDriver . disconnect ( ) ; } catch { /* ignore */ }
712- }
713- }
714-
715- if ( sqliteOk ) {
716- await kernel . use ( new DriverPlugin ( sqliteDriver ) ) ;
717- trackPlugin ( 'SqlDriver' ) ;
718- resolvedDriverLabel = 'SqlDriver(sqlite)' ;
719- resolvedDatabaseUrl = ':memory:' ;
720- } else {
721- // Native unavailable → try the pure-JS wasm SQLite driver before
722- // giving up on SQL fidelity entirely. Same probe-by-connect
723- // approach: actually open the connection so a load failure is
724- // caught here rather than on the first real query.
725- let wasmDriver : any ;
726- let wasmOk = false ;
727- try {
728- const { SqliteWasmDriver } = await import ( '@objectstack/driver-sqlite-wasm' ) ;
729- wasmDriver = new SqliteWasmDriver ( {
730- filename : ':memory:' ,
731- persist : 'on-disconnect' ,
732- } ) ;
733- await wasmDriver . connect ( ) ;
734- wasmOk = true ;
735- } catch {
736- wasmOk = false ;
737- if ( wasmDriver ?. disconnect ) {
738- try { await wasmDriver . disconnect ( ) ; } catch { /* ignore */ }
739- }
740- }
741-
742- if ( wasmOk ) {
743- await kernel . use ( new DriverPlugin ( wasmDriver ) ) ;
744- trackPlugin ( 'SqliteWasmDriver' ) ;
745- resolvedDriverLabel = 'SqliteWasmDriver' ;
746- resolvedDatabaseUrl = ':memory:' ;
747- console . warn ( chalk . yellow (
748- ' ⚠ native better-sqlite3 unavailable (ABI mismatch or not built) — dev using wasm SQLite (real SQL, slower).\n' +
749- ' Rebuild better-sqlite3 for native speed, or set OS_DATABASE_DRIVER=sqlite-wasm to silence this.'
750- ) ) ;
751- } else {
752- const { InMemoryDriver } = await import ( '@objectstack/driver-memory' ) ;
753- await kernel . use ( new DriverPlugin ( new InMemoryDriver ( ) ) ) ;
754- trackPlugin ( 'MemoryDriver' ) ;
755- resolvedDriverLabel = 'InMemoryDriver' ;
756- resolvedDatabaseUrl = '(in-memory)' ;
757- console . warn ( chalk . yellow (
758- ' ⚠ neither native nor wasm SQLite available — dev falling back to InMemoryDriver (mingo, not real SQL).\n' +
759- ' Rebuild better-sqlite3, or set OS_DATABASE_URL / OS_DATABASE_DRIVER for SQL fidelity.'
760- ) ) ;
761- }
762- }
688+ // Default in dev (no DB configured): prefer native SQLite for
689+ // production-like SQL at native speed, with a graceful step-down to
690+ // wasm SQLite (real SQL + on-disk persistence) then in-memory when the
691+ // native better-sqlite3 binary is unavailable — not built, ABI mismatch
692+ // after a Node upgrade (e.g. NODE_MODULE_VERSION change), or a blocked
693+ // prebuild download. Shared with the explicit-file branch and the
694+ // datasource factory via resolveSqliteDriver (#2229), which probes by
695+ // actually opening a connection + running SELECT 1 (better-sqlite3 loads
696+ // its native addon lazily at first query, not at construction).
697+ const { resolveSqliteDriver } = await import ( '@objectstack/service-datasource' ) ;
698+ const resolved = await resolveSqliteDriver ( {
699+ filename : ':memory:' ,
700+ dev : true ,
701+ autoMigrate : 'safe' , // #2186 dev loosen-only self-heal
702+ warn : ( m ) => console . warn ( chalk . yellow ( m ) ) ,
703+ } ) ;
704+ await kernel . use ( new DriverPlugin ( resolved . driver ) ) ;
705+ trackPlugin ( resolved . engine === 'memory' ? 'MemoryDriver' : resolved . engine === 'sqlite-wasm' ? 'SqliteWasmDriver' : 'SqlDriver' ) ;
706+ resolvedDriverLabel = resolved . label ;
707+ resolvedDatabaseUrl = resolved . engine === 'memory' ? '(in-memory)' : ':memory:' ;
763708 }
764709 } catch ( e : any ) {
765710 // silent
0 commit comments