Skip to content

Conversation

@i-am-the-slime
Copy link
Contributor

@i-am-the-slime i-am-the-slime commented Jan 4, 2026

Replace better-sqlite3 with Node.js's built-in node:sqlite module to avoid native compilation dependencies.

Changes

  • Replaced @libsql/client (turso) with Node.js native node:sqlite module
  • Removed external dependency from package.json
  • Implementation closely follows the original better-sqlite3 code style
  • Added explicit directory creation and Promise wrapping for FFI compatibility
  • Suppressed experimental feature warnings in the entry point

Benefits

  • Zero external dependencies for SQLite functionality
  • No native compilation required (no node-gyp)
  • Built-in Node.js module - will stabilize over time
  • Minimal code changes from the original better-sqlite3 implementation
  • Same performance - local file-based SQLite database

Testing

All tests pass: 235/239 (same as baseline - 4 pre-existing failures unrelated to SQLite changes)

Note

The node:sqlite module is marked as experimental in Node.js, but it's stable enough for our use case. The warning is suppressed in the CLI entry point.

@wclr
Copy link
Contributor

wclr commented Jan 5, 2026

Maybe it is better to move to nodejs build-in sql support https://nodejs.org/api/sqlite.html (available from Node 22)

Spago next is a new tool and still not widely used so I don't think that there need to worry about old node version and ignore node platform development.

@i-am-the-slime
Copy link
Contributor Author

@wclr sure, let's get some buy-in first from the maintainers and then we can also go that route.

Main motivation is to avoid having to rebuild spago for different node versions.

Replaces @libsql/client with Node.js's built-in node:sqlite module.

Changes:
- Replace libsql/turso client with DatabaseSync from node:sqlite
- Remove @libsql/client dependency from package.json
- Remove --external:@libsql/client from bundler config
- Add NODE_NO_WARNINGS=1 to suppress experimental sqlite warnings
- Add explicit directory creation (was built-in with better-sqlite3)
- Use db.exec() for PRAGMA statements (db.pragma() doesn't exist)
- Wrap all operations in Promise.resolve() for FFI compatibility

Benefits:
- Zero external dependencies for SQLite
- Simpler implementation (no remote database support needed)
- Nearly identical API to the original better-sqlite3 version

All tests pass (235/239, same as baseline - 4 pre-existing failures
unrelated to SQLite changes).
@i-am-the-slime i-am-the-slime changed the title Use turso instead of better-sqlite to avoid gyp dependency Use Node.js native sqlite instead of better-sqlite3 Jan 12, 2026
- Add missing getMetadataImpl export (was declared in FFI but not exported)
- Use --no-warnings flag to suppress experimental SQLite warnings
  - In bin/index.dev.js shebang for direct execution
  - In test/Prelude.purs when spawning spago in tests
- Add trailing newline to Db.js
bin/index.dev.js Outdated
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env -S node --no-warnings
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing all the warnings is a terrible idea for a set of reasons 😄

Let's just exclude the warnings for experimental modules: --no-warnings=ExperimentalWarning

FS.mkdirp Paths.globalCachePath
logDebug $ "Global cache: " <> Path.quote Paths.globalCachePath

-- Make sure we have git and purs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node will fail with a horrible error if it doesn't find the module, so I think we should catch it at the startup and error out then.

Implementation sketch:

import Data.Int as Int
import Data.String as String
import Effect.Console as Console
import Node.Process as Process

-- first thing inside main:
main = do
  -- Check that we're running on Node.js >= 22.5.0 (required for node:sqlite)
  let
    -- version is like "v22.5.0" or "22.5.0"
    versionStr = String.stripPrefix (String.Pattern "v") Process.version
      # fromMaybe Process.version
    parts = String.split (String.Pattern ".") versionStr

  case Array.take 2 parts >>= traverse Int.fromString of
    Just [major, minor]
      | major > 22 -> pure unit
      | major == 22 && minor >= 5 -> pure unit
      | otherwise -> do
        Console.error $ "Error: spago requires Node.js v22.5.0 or later (found " <> Process.version <> ")"
        Process.exit' 1
    _ -> do
      -- couldn't parse, issue a warning and let it crash later
      ..

together with this we should also add the engines field to the package.json. Node won't enforce it on install, but will complain about it:

 {                                                                                                      
    "engines": {                                                                                         
      "node": ">=22.5.0"                                                                                 
    }                                                                                                    
  }  

src/Spago/Db.js Outdated
fs.mkdirSync(dir, { recursive: true });
}
} catch (err) {
// ignore - will fail later if needed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we ignoring the error here?

src/Spago/Db.js Outdated
Comment on lines 78 to 79
.prepare("SELECT * FROM package_sets WHERE compiler = ? ORDER BY date DESC LIMIT 1")
.get(compiler);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use positional arguments in a few places, we should use them everywhere, e.g.:

Suggested change
.prepare("SELECT * FROM package_sets WHERE compiler = ? ORDER BY date DESC LIMIT 1")
.get(compiler);
.prepare("SELECT * FROM package_sets WHERE compiler = @compiler ORDER BY date DESC LIMIT 1")
.get({ compiler });

src/Spago/Db.js Outdated
.all();
return row;
}
return Promise.resolve().then(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing code is in Effect, and this database code is sync as well, why turn everything into Promises?

@i-am-the-slime i-am-the-slime marked this pull request as draft January 23, 2026 09:28
- Use Node.js native node:sqlite module (requires Node >= 22.5.0).
- Refactor DB layer to use synchronous API and named parameters.
- Add Node.js version check at startup.
- Suppress ExperimentalWarning in dev and bundle entry points.
- Address review comments from @f-f.
@i-am-the-slime i-am-the-slime force-pushed the turso-libsql branch 2 times, most recently from af35edb to ee76757 Compare January 23, 2026 22:34
- Use Node.js native node:sqlite module (requires Node >= 22.5.0).
- Refactor DB layer to use synchronous API and named parameters.
- Add Node.js version check at startup with helpful warning.
- Scope ExperimentalWarning suppression to the database connection logic.
- Update package.json engines field.
- Update bin/index.dev.js shebang to suppress ExperimentalWarning.
- Address review comments from @f-f.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants