Skip to content

Commit

Permalink
✨ Introduce namespaces
Browse files Browse the repository at this point in the history
Namespaces allow you to use Anyquery as a library rather than a CLI.
Create a namespace, load some plugins and be handed back a sql.DB pointer.
Under the hood, it's simply a wrapper around go-sqlite3 and the virtual tables of anyquery.
  • Loading branch information
julien040 committed Apr 20, 2024
1 parent fcdd4c4 commit 9300b77
Show file tree
Hide file tree
Showing 5 changed files with 496 additions and 3 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/julien040/anyquery

go 1.22.1

replace github.com/mattn/go-sqlite3 => github.com/julien040/go-sqlite3-anyquery v1.16.22
replace github.com/mattn/go-sqlite3 => github.com/julien040/go-sqlite3-anyquery v1.17.0

require (
github.com/gammazero/deque v0.2.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/julien040/go-sqlite3-anyquery v1.16.22 h1:Ca6q+pkpBQQocK1+06umBNfwWL+KkyxH/Cvn3qeOkM8=
github.com/julien040/go-sqlite3-anyquery v1.16.22/go.mod h1:9t7/JQ99yNR2b18cBR11VGyrJYg+Q7IQNHMWoCt6yGE=
github.com/julien040/go-sqlite3-anyquery v1.17.0 h1:agkknNjaUc9gbTONaMrPvCoJXBoJ84CoRSXaMh+IUv4=
github.com/julien040/go-sqlite3-anyquery v1.17.0/go.mod h1:9t7/JQ99yNR2b18cBR11VGyrJYg+Q7IQNHMWoCt6yGE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
Expand Down
243 changes: 243 additions & 0 deletions namespace/namespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package namespace

import (
"database/sql"
"errors"
"math/rand/v2"
"strconv"
"strings"

"github.com/julien040/anyquery/module"
"github.com/julien040/anyquery/rpc"
"github.com/mattn/go-sqlite3"
)

type NamespaceConfig struct {
// If InMemory is set to true, the SQLite database will only be stored in memory
InMemory bool

// The path to the SQLite database to open
//
// If InMemory is set to true, this field will be ignored
Path string

// The connection string to use to connect to the database
//
// If set, InMemory and Path will be ignored
ConnectionString string

// The page cache size in kilobytes
//
// By default, it is set to 50000 KB (50 MB)
PageCacheSize int

// Enforce foreign key constraints
EnforceForeignKeys bool
}

type Namespace struct {
// Unexported fields

// Check if the namespace was registered (the database/sql package was registered)
// If so, we cannot register any more plugins
//
// It's to prevent registering plugins that won't be used because the db connection is already opened
registered bool

// The connection string to use to connect to SQLite
connectionString string

// The list of plugins to load
goPluginToLoad []goPlugin

// The list of shared objects to load
sharedObjectToLoad []sharedObjectExtension
}

type sharedObjectExtension struct {
// Unexported fields
path string
entryPoint string
}

type goPlugin struct {
// Unexported fields
plugin sqlite3.Module
name string
}

func (n *Namespace) Init(config NamespaceConfig) error {
// Construct the connection string
connectionStringBuilder := strings.Builder{}
if config.ConnectionString != "" {
connectionStringBuilder.WriteString(config.ConnectionString)
} else {
if config.InMemory || config.Path == "" {
config.Path = "anyquery.db" // If in memory, we use a default path that will be ignored
}
connectionStringBuilder.WriteString("file:")
connectionStringBuilder.WriteString(config.Path)

// Set shared cache to true
connectionStringBuilder.WriteString("?cache=shared")

// Open the database in memory if needed
if config.InMemory {
connectionStringBuilder.WriteString("&mode=memory")
}

// Set the page cache size
connectionStringBuilder.WriteString("&_cache_size=")
if config.PageCacheSize > 0 {
// To indicate a value in KB, we have to return a negative value
connectionStringBuilder.WriteString(strconv.Itoa((-1) * config.PageCacheSize))
} else {
connectionStringBuilder.WriteString("-50000")
}

// Set the journal mode to WAL and synchronous to NORMAL
connectionStringBuilder.WriteString("&_journal_mode=WAL")
connectionStringBuilder.WriteString("&_synchronous=NORMAL")

// Set the foreign key constraints
if config.EnforceForeignKeys {
connectionStringBuilder.WriteString("&_foreign_keys=ON")
} else {
connectionStringBuilder.WriteString("&_foreign_keys=OFF")
}
}

result := connectionStringBuilder.String()
n.connectionString = result

return nil
}

func NewNamespace(config NamespaceConfig) (*Namespace, error) {
n := &Namespace{}
err := n.Init(config)
if err != nil {
return nil, err
}
return n, nil
}

// Load a plugin written in Go
//
// Note: the plugin will only be loaded once the namespace is registered
func (n *Namespace) LoadGoPlugin(plugin sqlite3.Module, name string) error {
if n.registered {
return errors.New("the namespace is already registered. Go plugin must be loaded before registering the namespace")
}
n.goPluginToLoad = append(n.goPluginToLoad, goPlugin{plugin: plugin, name: name})
return nil
}

// Load a SQLite extension built as a shared object (.so)
//
// Note: the plugin will only be loaded once the namespace is registered
func (n *Namespace) LoadSharedExtension(path string, entrypoint string) error {
/* if entrypoint == "" {
// https://www.sqlite.org/c3ref/load_extension.html#:~:text=The%20entry%20point%20is%20zProc.%20zProc%20may%20be%200
entrypoint = "0"
} */
if path == "" {
return errors.New("the path of the shared object cannot be empty")
}
if n.registered {
return errors.New("the namespace is already registered. Shared extension must be loaded before registering the namespace")
}
n.sharedObjectToLoad = append(n.sharedObjectToLoad, sharedObjectExtension{entryPoint: entrypoint, path: path})
return nil
}

// Register a plugin written in Go built for anyquery for each table of the manifest
//
// In the manifest, any zeroed string of table name will be ignored
func (n *Namespace) LoadAnyqueryPlugin(path string, manifest rpc.PluginManifest, userConfig map[string]string) error {
if path == "" {
return errors.New("the path of the plugin cannot be empty")
}
if n.registered {
return errors.New("the namespace is already registered. Anyquery plugin must be loaded before registering the namespace")
}

// Load the plugin
for index, table := range manifest.Tables {
plugin := &module.SQLiteModule{
PluginPath: path,
PluginManifest: manifest,
TableIndex: index,
UserConfig: userConfig,
}
n.LoadGoPlugin(plugin, table)
}
return nil
}

// Register registers the namespace to the database/sql package
//
// It takes the name of the connection to register. If not specified, a random name will be generated
func (n *Namespace) Register(registerName string) (*sql.DB, error) {
if n.registered {
return nil, errors.New("the namespace is already registered")
}

// Check if the connection string is not empty
if n.connectionString == "" {
return nil, errors.New("the connection string cannot be empty. You must init the namespace before registering it")
}

// Check if the register name is empty
if registerName == "" {
registerName = "anyquery_custom" + strconv.Itoa(rand.Int())
}

for _, driver := range sql.Drivers() {
if driver == registerName {
return nil, errors.New("the connection string is already registered")
}
}

// Register the database/sql package
sql.Register(registerName, &sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) error {

// We load the shared objects
for _, sharedObject := range n.sharedObjectToLoad {
err := conn.LoadExtension(sharedObject.path, sharedObject.entryPoint)
if err != nil {
return err
}
}

// We load the Go plugins
for _, goPlugin := range n.goPluginToLoad {
err := conn.CreateModule(goPlugin.name, goPlugin.plugin)
if err != nil {
return err
}
}

return nil
},
})

// Create the DB connection
db, err := sql.Open(registerName, n.connectionString)
if err != nil {
return nil, err
}

// go-sqlite3 is not thread-safe for writing
db.SetMaxOpenConns(1)

n.registered = true

return db, nil

}

func (n *Namespace) GetConnectionString() string {
return n.connectionString
}
Loading

0 comments on commit 9300b77

Please sign in to comment.