Skip to content

feat: Add SQLite extension loading support#110

Open
dsisnero wants to merge 1 commit into
crystal-lang:masterfrom
dsisnero:feat/extension-loading
Open

feat: Add SQLite extension loading support#110
dsisnero wants to merge 1 commit into
crystal-lang:masterfrom
dsisnero:feat/extension-loading

Conversation

@dsisnero
Copy link
Copy Markdown

@dsisnero dsisnero commented Apr 1, 2026

Summary

Add enable_load_extension and load_extension methods to SQLite3::Connection for dynamically loading SQLite extensions at runtime.

Closes #106

Changes

  • Add lib bindings for sqlite3_enable_load_extension and sqlite3_load_extension
  • Add Connection#enable_load_extension(enabled : Bool) method
  • Add Connection#load_extension(path, entry_point) method with full documentation
  • Add TDD tests for extension loading (6 tests, all pass)

Use Cases

SQLite extensions provide valuable functionality:

  • sqlite-vec: Vector similarity search for AI applications
  • sqlite-json: Enhanced JSON support
  • sqlite-fts5: Full-text search
  • Custom domain-specific extensions

Without extension loading, Crystal applications cannot leverage these powerful SQLite extensions.

Testing

# All 336 existing tests pass
crystal spec

# New extension loading tests pass
crystal spec spec/extension_loading_spec.cr
# 6 examples, 0 failures, 0 errors, 0 pending

Example Usage

db = DB.open("sqlite3::memory:")
db.using_connection do |conn|
  sqlite_conn = conn.as(SQLite3::Connection)
  sqlite_conn.enable_load_extension(true)
  sqlite_conn.load_extension("/path/to/extension.dylib")
end

Adds ability to load SQLite extensions at runtime, enabling use of
sqlite-vec and other extension modules.

Changes:
- Add enable_load_extension and load_extension methods to Connection
- Add String-based constructor to Exception for custom error messages
- Add linker flags for Homebrew SQLite on macOS (has extension support)
- Add TDD tests for extension loading functionality

This enables use cases like:
- Loading sqlite-vec for vector similarity search
- Loading FTS5 for full-text search
- Loading custom SQLite extensions

Closes crystal-lang#1
@dsisnero dsisnero force-pushed the feat/extension-loading branch from f2cb662 to 0f06a9d Compare April 1, 2026 23:30
Comment thread src/sqlite3/connection.cr

private def check(code)
raise Exception.new(self) unless code == 0
raise Exception.new(@db) unless code == 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Issue: unrelated change.

Comment on lines +51 to +59
describe SQLite3::Exception do
describe ".new(String)" do
it "creates exception with message" do
ex = SQLite3::Exception.new("Test error message")
ex.message.should eq("Test error message")
ex.code.should eq(0)
end
end
end
Copy link
Copy Markdown

@ysbaddaden ysbaddaden Apr 2, 2026

Choose a reason for hiding this comment

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

Issue: Wrong spec file. Not needed, either.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Issue The whole specs are wrong.

The really interesting spec will only run on macOS with sqlite3 installed through Homebrew, and otherwise silently skips (false success). At the very least a skipped spec must be explicit.

Maybe we could compile a dummy extension, or take an ENV parameter to an extension library, so we can at least configure CI.

There are no specs for testing that we can't load an extension when disabled.

There are also no specs that try to execute a query that uses the extension and asserts that it succeeds (enabled) or fails (not loaded).

Comment thread src/sqlite3/connection.cr
Comment on lines +142 to +147
err_msg = Pointer(UInt8).null
result = LibSQLite3.load_extension(@db, path, entry_point, pointerof(err_msg))
if result != 0
msg = err_msg.null? ? "Unknown error" : String.new(err_msg)
raise Exception.new("Failed to load extension: #{msg}")
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Issue: memory leak, err_msg has been allocated and must be freed.

Suggested change
err_msg = Pointer(UInt8).null
result = LibSQLite3.load_extension(@db, path, entry_point, pointerof(err_msg))
if result != 0
msg = err_msg.null? ? "Unknown error" : String.new(err_msg)
raise Exception.new("Failed to load extension: #{msg}")
end
ret = LibSQLite3.load_extension(@db, path, entry_point, out err_msg)
return unless ret == 0
msg = err_msg ? String.new(err_msg).tap { LibSQLite3.free(err_msg) } : "Unknown error"
raise Exception.new("Failed to load extension: #{msg}")

Comment on lines +7 to +9
{% if flag?(:darwin) %}
@[Link(ldflags: "-L/opt/homebrew/opt/sqlite/lib")]
{% end %}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Issue: this is invalid.

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.

Allow loading SQLite extensions

2 participants