Skip to content

Commit cc0e520

Browse files
committed
✨ Add developer mode
1 parent d540f08 commit cc0e520

11 files changed

+488
-4
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
*.out
2222
go.work
2323
main
24+
*.db
2425

2526
# GoLand+all template downloaded with gut
2627
.idea/**/workspace.xml

cmd/query.go

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func init() {
2323
queryCmd.Flags().Bool("readonly", false, "Start the server in read-only mode")
2424
queryCmd.Flags().Bool("read-only", false, "Start the server in read-only mode")
2525
queryCmd.Flags().StringArray("init", []string{}, "Run SQL commands in a file before the query. You can specify multiple files.")
26+
queryCmd.Flags().Bool("dev", false, "Run the program in developer mode")
2627

2728
// Query flags
2829
queryCmd.Flags().StringP("query", "q", "", "Query to run")

cmd/root.go

+17
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,21 @@ func Execute() {
2929
func init() {
3030
rootCmd.Flags().Bool("no-input", false, "Do not launch an interactive input")
3131
rootCmd.Flags().BoolP("version", "v", false, "Print the version of the program")
32+
33+
addFlag_commandModifiesConfiguration(rootCmd)
34+
addFlag_commandPrintsData(rootCmd)
35+
rootCmd.Flags().StringP("database", "d", "anyquery.db", "Database to connect to (a path or :memory:)")
36+
rootCmd.Flags().Bool("in-memory", false, "Use an in-memory database")
37+
rootCmd.Flags().Bool("readonly", false, "Start the server in read-only mode")
38+
rootCmd.Flags().Bool("read-only", false, "Start the server in read-only mode")
39+
rootCmd.Flags().StringArray("init", []string{}, "Run SQL commands in a file before the query. You can specify multiple files.")
40+
rootCmd.Flags().Bool("dev", false, "Run the program in developer mode")
41+
42+
// Query flags
43+
rootCmd.Flags().StringP("query", "q", "", "Query to run")
44+
45+
// Log flags
46+
rootCmd.Flags().String("log-file", "", "Log file")
47+
rootCmd.Flags().String("log-level", "info", "Log level (trace, debug, info, warn, error, off)")
48+
rootCmd.Flags().String("log-format", "text", "Log format (text, json)")
3249
}

controller/query.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ CREATE VIEW IF NOT EXISTS dual AS SELECT 'x' AS dummy;
2222

2323
func Query(cmd *cobra.Command, args []string) error {
2424
path := "anyquery.db"
25-
var inMemory, readOnly bool
25+
var inMemory, readOnly, devMode bool
2626

2727
// Get the flags
2828
path, _ = cmd.Flags().GetString("database")
@@ -54,6 +54,8 @@ func Query(cmd *cobra.Command, args []string) error {
5454
path = "myrandom.db"
5555
}
5656

57+
devMode, _ = cmd.Flags().GetBool("dev")
58+
5759
// Create the logger
5860
var outputLog io.Writer
5961
logFile, _ := cmd.Flags().GetString("log-file")
@@ -83,6 +85,7 @@ func Query(cmd *cobra.Command, args []string) error {
8385
Level: logLevel,
8486
},
8587
),
88+
DevMode: devMode,
8689
})
8790
if err != nil {
8891
return fmt.Errorf("failed to create namespace: %w", err)

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module github.com/julien040/anyquery
22

33
go 1.22.3
44

5-
replace github.com/mattn/go-sqlite3 => github.com/julien040/go-sqlite3-anyquery v1.17.0
5+
replace github.com/mattn/go-sqlite3 => github.com/julien040/go-sqlite3-anyquery v1.17.1
66

77
require (
88
github.com/Masterminds/semver/v3 v3.2.1

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -425,8 +425,8 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
425425
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
426426
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
427427
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
428-
github.com/julien040/go-sqlite3-anyquery v1.17.0 h1:agkknNjaUc9gbTONaMrPvCoJXBoJ84CoRSXaMh+IUv4=
429-
github.com/julien040/go-sqlite3-anyquery v1.17.0/go.mod h1:9t7/JQ99yNR2b18cBR11VGyrJYg+Q7IQNHMWoCt6yGE=
428+
github.com/julien040/go-sqlite3-anyquery v1.17.1 h1:bowoAX6uWXqnE7QYc13FhFEoXCLSNHPwDWnUxr0cBQw=
429+
github.com/julien040/go-sqlite3-anyquery v1.17.1/go.mod h1:9t7/JQ99yNR2b18cBR11VGyrJYg+Q7IQNHMWoCt6yGE=
430430
github.com/julien040/go-ternary v0.0.0-20230119180150-f0435f66948e h1:q8lhYSYDzN8slDRCVRt2TD2ShyjNcuQU+I9LZNPv4TM=
431431
github.com/julien040/go-ternary v0.0.0-20230119180150-f0435f66948e/go.mod h1:XXIcjDHL7vyuHA7V0UwaTKMscsqKzFkE9FTGbBeqJHM=
432432
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=

namespace/dev_manifest.json

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"type": "object",
4+
"properties": {
5+
"executable": {
6+
"type": "string"
7+
},
8+
"tables": {
9+
"type": "array",
10+
"items": {
11+
"type": "string"
12+
}
13+
},
14+
"user_config": {
15+
"type": ["object"],
16+
"minProperties": 1,
17+
"additionalProperties": {
18+
"type": ["object"],
19+
"additionalProperties": {
20+
"type": ["string", "number", "boolean", "array"]
21+
}
22+
}
23+
},
24+
"is_shared_extension": {
25+
"type": "boolean"
26+
},
27+
"log_level": {
28+
"type": "string",
29+
"enum": ["trace", "debug", "info", "warn", "error", ""]
30+
},
31+
"log_file": {
32+
"type": "string"
33+
}
34+
},
35+
"required": ["executable", "tables", "user_config"]
36+
}

namespace/developer.go

+243
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// Define SQL functions that helps to develop plugins
2+
package namespace
3+
4+
import (
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"os"
9+
"strings"
10+
11+
"github.com/hashicorp/go-hclog"
12+
"github.com/julien040/anyquery/module"
13+
"github.com/julien040/anyquery/rpc"
14+
"github.com/mattn/go-sqlite3"
15+
"github.com/santhosh-tekuri/jsonschema/v5"
16+
17+
_ "embed"
18+
)
19+
20+
//go:embed dev_manifest.json
21+
var devManifestSchema string
22+
23+
type manifest struct {
24+
// Fields that are filled by the function and are therefore ignored by the unmarshal
25+
Name string `json:"-"` // Name of the plugin
26+
FdLog *os.File `json:"-"` // File descriptor for the log file
27+
ListOfTables []string `json:"-"` // List of modules that are created by the plugin
28+
ManifestPath string `json:"-"` // Path to the manifest file
29+
30+
// Fields that are filled by the user
31+
Executable string `json:"executable"`
32+
UserConfig map[string]map[string]interface{} `json:"user_config"`
33+
TableNames []string `json:"tables"`
34+
IsSharedExtension bool `json:"is_shared_extension"`
35+
LogFile string `json:"log_file"`
36+
LogLevel string `json:"log_level"`
37+
}
38+
39+
type devFunction struct {
40+
conn *sqlite3.SQLiteConn
41+
manifests map[string]manifest
42+
connectionPool *rpc.ConnectionPool
43+
}
44+
45+
func (f *devFunction) LoadDevPlugin(args ...string) string {
46+
schema, err := jsonschema.CompileString("dev_manifest.json", devManifestSchema)
47+
if err != nil {
48+
return fmt.Sprintf("error compiling schema\n%v", err)
49+
}
50+
51+
if len(args) < 2 {
52+
return "error: not enough arguments"
53+
}
54+
55+
pluginName := args[0]
56+
pluginManifestPath := args[1]
57+
58+
// Validate the manifest and read it
59+
rawManifest, err := os.ReadFile(pluginManifestPath)
60+
if err != nil {
61+
return fmt.Sprintf("error reading manifest\n%v", err)
62+
}
63+
64+
var unmarshaled map[string]interface{}
65+
err = json.Unmarshal(rawManifest, &unmarshaled)
66+
if err != nil {
67+
return fmt.Sprintf("error reading json manifest\n%v", err)
68+
}
69+
err = schema.Validate(unmarshaled)
70+
if err != nil {
71+
return fmt.Sprintf("error validating manifest\n%v", err)
72+
}
73+
74+
// Load the manifest properly
75+
var m manifest
76+
json.Unmarshal(rawManifest, &m)
77+
m.Name = pluginName
78+
m.ManifestPath = pluginManifestPath
79+
80+
parsedArgs := parseCommands(m.Executable)
81+
path := parsedArgs[0]
82+
args = parsedArgs[1:]
83+
84+
outputLog := io.Discard
85+
86+
if m.LogFile != "" {
87+
logFile, err := os.OpenFile(m.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
88+
if err != nil {
89+
return fmt.Sprintf("error opening log file\n%v", err)
90+
}
91+
outputLog = logFile
92+
m.FdLog = logFile
93+
}
94+
95+
if m.LogLevel == "" {
96+
m.LogLevel = "info"
97+
}
98+
99+
// Set the log file and log level
100+
logger := hclog.New(&hclog.LoggerOptions{
101+
Name: "dev-plugin " + pluginName,
102+
Level: hclog.LevelFromString(m.LogLevel),
103+
Output: outputLog,
104+
})
105+
106+
i := 0
107+
tableNames := []string{}
108+
for profileName, profile := range m.UserConfig {
109+
// Table name is <profile_name>_<plugin_name>_<table_name>
110+
// unless the profile name is "default"
111+
tableNamePrefix := ""
112+
if profileName != "default" {
113+
tableNamePrefix = profileName + "_"
114+
}
115+
116+
for tableIndex, tableName := range m.TableNames {
117+
tableName = tableNamePrefix + pluginName + "_" + tableName
118+
moduleToLoad := &module.SQLiteModule{
119+
PluginPath: path,
120+
PluginArgs: args,
121+
UserConfig: profile,
122+
PluginManifest: rpc.PluginManifest{
123+
Name: pluginName,
124+
Tables: m.TableNames,
125+
Description: "Dev plugin " + pluginName,
126+
Author: "An awesome developer",
127+
Version: "0.0.1",
128+
},
129+
TableIndex: tableIndex,
130+
ConnectionIndex: i,
131+
ConnectionPool: f.connectionPool,
132+
Stderr: m.FdLog,
133+
Logger: logger,
134+
}
135+
136+
err = f.conn.CreateModule(tableName, moduleToLoad)
137+
if err != nil {
138+
return fmt.Sprintf("error creating module\n%v", err)
139+
}
140+
141+
tableNames = append(tableNames, tableName)
142+
}
143+
// Increment the connection index
144+
i++
145+
}
146+
147+
m.ListOfTables = tableNames
148+
f.manifests[pluginName] = m
149+
150+
returnMessage := strings.Builder{}
151+
returnMessage.WriteString("Successfully loaded plugin " + pluginName + "\n")
152+
returnMessage.WriteString("Tables:\n")
153+
for _, tableName := range tableNames {
154+
returnMessage.WriteString(tableName + "\n")
155+
}
156+
return returnMessage.String()
157+
}
158+
159+
func (f *devFunction) UnloadDevPlugin(name string) string {
160+
// Get the manifest
161+
manifest, ok := f.manifests[name]
162+
if !ok {
163+
return "Dev plugin " + name + " not found"
164+
}
165+
166+
// Unload the plugin
167+
for _, tableName := range manifest.ListOfTables {
168+
err := f.conn.DropModule(tableName)
169+
if err != nil {
170+
return err.Error()
171+
}
172+
}
173+
174+
// Close the file opened for stderr
175+
if manifest.FdLog != nil {
176+
manifest.FdLog.Close()
177+
}
178+
179+
// Remove the manifest from the map
180+
delete(f.manifests, name)
181+
182+
// Return the success message
183+
return "Successfully unloaded plugin " + name
184+
}
185+
186+
func (f *devFunction) ReloadDevPlugin(name string) string {
187+
// Unload the plugin
188+
unloadMessage := f.UnloadDevPlugin(name)
189+
if !strings.Contains(unloadMessage, "Successfully unloaded plugin") {
190+
return unloadMessage
191+
}
192+
193+
// Load the plugin
194+
loadMessage := f.LoadDevPlugin(name, name+".json")
195+
if !strings.Contains(loadMessage, "Successfully loaded plugin") {
196+
return loadMessage
197+
}
198+
199+
return "Successfully reloaded plugin " + name
200+
}
201+
202+
func (f *devFunction) ListDevPlugins() string {
203+
message := strings.Builder{}
204+
message.WriteString("List of loaded dev plugins:\n")
205+
for name := range f.manifests {
206+
message.WriteString(name + " " + f.manifests[name].ManifestPath + "\n")
207+
}
208+
return message.String()
209+
}
210+
211+
func parseCommands(executableArg string) []string {
212+
args := []string{}
213+
tempArg := strings.Builder{}
214+
doubleQuoteEscape := false
215+
mustAppend := false
216+
for _, c := range executableArg {
217+
switch c {
218+
case ' ':
219+
if doubleQuoteEscape {
220+
tempArg.WriteRune(c)
221+
} else {
222+
mustAppend = true
223+
}
224+
225+
case '"':
226+
doubleQuoteEscape = !doubleQuoteEscape
227+
228+
default:
229+
tempArg.WriteRune(c)
230+
}
231+
232+
if mustAppend {
233+
args = append(args, tempArg.String())
234+
tempArg.Reset()
235+
mustAppend = false
236+
}
237+
238+
}
239+
if tempArg.Len() > 0 {
240+
args = append(args, tempArg.String())
241+
}
242+
return args
243+
}

0 commit comments

Comments
 (0)