diff --git a/v2/db.go b/v2/db.go new file mode 100644 index 0000000..4b49949 --- /dev/null +++ b/v2/db.go @@ -0,0 +1,507 @@ +package grnci + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "strings" + "time" +) + +// DB is a wrapper to provide a high-level command interface. +type DB struct { + Handler +} + +// NewDB returns a new DB that wraps the specified client or handle. +func NewDB(h Handler) *DB { + return &DB{Handler: h} +} + +// ColumnCreate executes column_create. +func (db *DB) ColumnCreate(tbl, name, typ, flags string) (bool, Response, error) { + req, err := NewRequest("column_create", map[string]interface{}{ + "table": tbl, + "name": name, + }, nil) + if err != nil { + return false, nil, err + } + typFlag := "COLUMN_SCALAR" + withSection := false + src := "" + if strings.HasPrefix(typ, "[]") { + typFlag = "COLUMN_VECTOR" + typ = typ[2:] + } else if idx := strings.IndexByte(typ, '.'); idx != -1 { + typFlag = "COLUMN_INDEX" + src = typ[idx+1:] + typ = typ[:idx] + if idx := strings.IndexByte(src, ','); idx != -1 { + withSection = true + } + } + if flags == "" { + flags = typFlag + } else { + flags += "|" + typFlag + } + if withSection { + flags += "|WITH_SECTION" + } + if err := req.AddParam("flags", flags); err != nil { + return false, nil, err + } + if err := req.AddParam("type", typ); err != nil { + return false, nil, err + } + if src != "" { + if err := req.AddParam("source", src); err != nil { + return false, nil, err + } + } + resp, err := db.Query(req) + if err != nil { + return false, nil, err + } + defer resp.Close() + jsonData, err := ioutil.ReadAll(resp) + if err != nil { + return false, resp, err + } + var result bool + if err := json.Unmarshal(jsonData, &result); err != nil { + return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + "method": "json.Unmarshal", + "error": err.Error(), + }) + } + return result, resp, nil +} + +// ColumnRemove executes column_remove. +func (db *DB) ColumnRemove(tbl, name string) (bool, Response, error) { + req, err := NewRequest("column_remove", map[string]interface{}{ + "table": tbl, + "name": name, + }, nil) + if err != nil { + return false, nil, err + } + resp, err := db.Query(req) + if err != nil { + return false, nil, err + } + defer resp.Close() + jsonData, err := ioutil.ReadAll(resp) + if err != nil { + return false, resp, err + } + var result bool + if err := json.Unmarshal(jsonData, &result); err != nil { + return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + "method": "json.Unmarshal", + "error": err.Error(), + }) + } + return result, resp, nil +} + +// DumpOptions stores options for DB.Dump. +type DumpOptions struct { + Tables string // --table + DumpPlugins string // --dump_plugins + DumpSchema string // --dump_schema + DumpRecords string // --dump_records + DumpIndexes string // --dump_indexes +} + +// NewDumpOptions returns the default DumpOptions. +func NewDumpOptions() *DumpOptions { + return &DumpOptions{ + DumpPlugins: "yes", + DumpSchema: "yes", + DumpRecords: "yes", + DumpIndexes: "yes", + } +} + +// Dump executes dump. +// On success, it is the caller's responsibility to close the response. +func (db *DB) Dump(options *DumpOptions) (Response, error) { + if options == nil { + options = NewDumpOptions() + } + params := map[string]interface{}{ + "dump_plugins": options.DumpPlugins, + "dump_schema": options.DumpSchema, + "dump_records": options.DumpRecords, + "dump_indexes": options.DumpIndexes, + } + if options.Tables != "" { + params["tables"] = options.Tables + } + req, err := NewRequest("dump", params, nil) + if err != nil { + return nil, err + } + return db.Query(req) +} + +// LoadOptions stores options for DB.Load. +// http://groonga.org/docs/reference/commands/load.html +type LoadOptions struct { + Columns string // --columns + IfExists string // --ifexists +} + +// Load executes load. +func (db *DB) Load(tbl string, values io.Reader, options *LoadOptions) (int, Response, error) { + params := map[string]interface{}{ + "table": tbl, + } + if options != nil { + if options.Columns != "" { + params["columns"] = options.Columns + } + if options.IfExists != "" { + params["ifexists"] = options.IfExists + } + } + req, err := NewRequest("load", params, values) + if err != nil { + return 0, nil, err + } + resp, err := db.Query(req) + if err != nil { + return 0, nil, err + } + defer resp.Close() + jsonData, err := ioutil.ReadAll(resp) + if err != nil { + return 0, resp, err + } + var result int + if err := json.Unmarshal(jsonData, &result); err != nil { + return 0, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + "method": "json.Unmarshal", + "error": err.Error(), + }) + } + return result, resp, nil +} + +// For --columns[NAME].stage, type, value. +// type SelectOptionsColumn struct { +// Stage string // --columns[NAME].stage +// Type string // --columns[NAME].type +// Value string // --columns[NAME].value +// } + +// For --drilldowns[LABEL].columns[NAME]. +// type SelectOptionsDorilldownColumn struct { +// Stage string // --drilldowns[LABEL].columns[NAME].stage +// Flags string // --drilldowns[LABEL].columns[NAME].flags +// Type string // --drilldowns[LABEL].columns[NAME].type +// Value string // --drilldowns[LABEL].columns[NAME].value +// WindowSortKeys string // --drilldowns[LABEL].columns[NAME].window.sort_keys +// WindowGroupKeys string // --drilldowns[LABEL].columns[NAME].window.group_keys +// } + +// For --drilldowns[LABEL].keys, sort_keys, output_columns, offset, limit, calc_types, calc_target, filter, columns[]. +// type SelectOptionsDrilldown struct { +// Keys string // --drilldowns[LABEL].keys +// SortKeys string // --drilldowns[LABEL].sort_keys +// OutputColumns string // --drilldowns[LABEL].output_columns +// Offset int // --drilldowns[LABEL].offset +// Limit int // --drilldowns[LABEL].limit +// CalcTypes string // --drilldowns[LABEL].calc_types +// CalcTarget string // --drilldowns[LABEL].calc_target +// Filter string // --drilldowns[LABEL].filter +// Columns map[string]*SelectOptionsDorilldownColumn +// } + +// NewSelectOptionsDrilldown returns the default SelectOptionsDrilldown. +// func NewSelectOptionsDrilldown() *SelectOptionsDrilldown { +// return &SelectOptionsDrilldown{ +// Limit: 10, +// } +// } + +// SelectOptions stores options for DB.Select. +// http://groonga.org/docs/reference/commands/select.html +type SelectOptions struct { + MatchColumns string // --match_columns + Query string // --query + Filter string // --filter + Scorer string // --scorer + SortKeys string // --sort_keys + OutputColumns string // --output_columns + Offset int // --offset + Limit int // --limit + Drilldown string // --drilldown + DrilldownSortKeys string // --drilldown_sort_keys + DrilldownOutputColumns string // --drilldown_output_columns + DrillDownOffset int // drilldown_offset + DrillDownLimit int // drilldown_limit + Cache bool // --cache + MatchEscalationThreshold int // --match_escalation_threshold + QueryExpansion string // --query_expansion + QueryFlags string // --query_flags + QueryExpander string // --query_expander + Adjuster string // --adjuster + DrilldownCalcTypes string // --drilldown_calc_types + DrilldownCalcTarget string // --drilldown_calc_target + DrilldownFilter string // --drilldown_filter + // Columns map[string]*SelectOptionsColumn // --columns[NAME] + // Drilldowns map[string]*SelectOptionsDrilldown // --drilldowns[LABEL] +} + +// NewSelectOptions returns the default SelectOptions. +func NewSelectOptions() *SelectOptions { + return &SelectOptions{ + Limit: 10, + DrillDownLimit: 10, + } +} + +// Select executes select. +// On success, it is the caller's responsibility to close the response. +func (db *DB) Select(tbl string, options *SelectOptions) (Response, error) { + if options == nil { + options = NewSelectOptions() + } + params := map[string]interface{}{ + "table": tbl, + } + // TODO: copy entries from options to params. + req, err := NewRequest("dump", params, nil) + if err != nil { + return nil, err + } + return db.Query(req) +} + +// StatusResult is a response of status. +type StatusResult struct { + AllocCount int `json:"alloc_count"` + CacheHitRate float64 `json:"cache_hit_rate"` + CommandVersion int `json:"command_version"` + DefaultCommandVersion int `json:"default_command_version"` + MaxCommandVersion int `json:"max_command_version"` + NQueries int `json:"n_queries"` + StartTime time.Time `json:"start_time"` + Uptime time.Duration `json:"uptime"` + Version string `json:"version"` +} + +// Status executes status. +func (db *DB) Status() (*StatusResult, Response, error) { + resp, err := db.Exec("status", nil) + if err != nil { + return nil, nil, err + } + defer resp.Close() + jsonData, err := ioutil.ReadAll(resp) + if err != nil { + return nil, resp, err + } + var data map[string]interface{} + if err := json.Unmarshal(jsonData, &data); err != nil { + return nil, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + "method": "json.Unmarshal", + "error": err.Error(), + }) + } + var result StatusResult + if v, ok := data["alloc_count"]; ok { + if v, ok := v.(float64); ok { + result.AllocCount = int(v) + } + } + if v, ok := data["cache_hit_rate"]; ok { + if v, ok := v.(float64); ok { + result.CacheHitRate = v + } + } + if v, ok := data["command_version"]; ok { + if v, ok := v.(float64); ok { + result.CommandVersion = int(v) + } + } + if v, ok := data["default_command_version"]; ok { + if v, ok := v.(float64); ok { + result.DefaultCommandVersion = int(v) + } + } + if v, ok := data["max_command_version"]; ok { + if v, ok := v.(float64); ok { + result.MaxCommandVersion = int(v) + } + } + if v, ok := data["n_queries"]; ok { + if v, ok := v.(float64); ok { + result.NQueries = int(v) + } + } + if v, ok := data["start_time"]; ok { + if v, ok := v.(float64); ok { + result.StartTime = time.Unix(int64(v), 0) + } + } + if v, ok := data["uptime"]; ok { + if v, ok := v.(float64); ok { + result.Uptime = time.Duration(time.Duration(v) * time.Second) + } + } + if v, ok := data["version"]; ok { + if v, ok := v.(string); ok { + result.Version = v + } + } + return &result, resp, nil +} + +// TableCreateOptions stores options for DB.TableCreate. +// http://groonga.org/docs/reference/commands/table_create.html +type TableCreateOptions struct { + Flags string // --flags + KeyType string // --key_type + ValueType string // --value_type + DefaultTokenizer string // --default_tokenizer + Normalizer string // --normalizer + TokenFilters string // --token_filters +} + +// TableCreate executes table_create. +func (db *DB) TableCreate(name string, options *TableCreateOptions) (bool, Response, error) { + if options == nil { + options = &TableCreateOptions{} + } + params := map[string]interface{}{ + "name": name, + } + flags, keyFlag := "", "" + if options.Flags != "" { + for _, flag := range strings.Split(options.Flags, "|") { + switch flag { + case "TABLE_NO_KEY": + if keyFlag != "" { + return false, nil, fmt.Errorf("TABLE_NO_KEY must not be set with %s", keyFlag) + } + if options.KeyType != "" { + return false, nil, fmt.Errorf("TABLE_NO_KEY disallows KeyType") + } + keyFlag = flag + case "TABLE_HASH_KEY", "TABLE_PAT_KEY", "TABLE_DAT_KEY": + if keyFlag != "" { + return false, nil, fmt.Errorf("%s must not be set with %s", flag, keyFlag) + } + if options.KeyType == "" { + return false, nil, fmt.Errorf("%s requires KeyType", flag) + } + keyFlag = flag + } + } + flags = options.Flags + } + if keyFlag == "" { + if options.KeyType == "" { + keyFlag = "TABLE_NO_KEY" + } else { + keyFlag = "TABLE_HASH_KEY" + } + if flags == "" { + flags = keyFlag + } else { + flags += "|" + keyFlag + } + } + if flags != "" { + params["flags"] = flags + } + if options.KeyType != "" { + params["key_type"] = options.KeyType + } + if options.ValueType != "" { + params["value_type"] = options.ValueType + } + if options.DefaultTokenizer != "" { + params["default_tokenizer"] = options.DefaultTokenizer + } + if options.Normalizer != "" { + params["normalizer"] = options.Normalizer + } + if options.TokenFilters != "" { + params["token_filters"] = options.TokenFilters + } + resp, err := db.Invoke("table_create", params, nil) + if err != nil { + return false, nil, err + } + defer resp.Close() + jsonData, err := ioutil.ReadAll(resp) + if err != nil { + return false, resp, err + } + var result bool + if err := json.Unmarshal(jsonData, &result); err != nil { + return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + "method": "json.Unmarshal", + "error": err.Error(), + }) + } + return result, resp, nil +} + +// TableRemove executes table_remove. +func (db *DB) TableRemove(name string, dependent bool) (bool, Response, error) { + req, err := NewRequest("table_remove", map[string]interface{}{ + "name": name, + "dependent": dependent, + }, nil) + if err != nil { + return false, nil, err + } + resp, err := db.Query(req) + if err != nil { + return false, nil, err + } + defer resp.Close() + jsonData, err := ioutil.ReadAll(resp) + if err != nil { + return false, resp, err + } + var result bool + if err := json.Unmarshal(jsonData, &result); err != nil { + return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + "method": "json.Unmarshal", + "error": err.Error(), + }) + } + return result, resp, nil +} + +// Truncate executes truncate. +func (db *DB) Truncate(target string) (bool, Response, error) { + resp, err := db.Invoke("truncate", map[string]interface{}{ + "target_name": target, + }, nil) + if err != nil { + return false, nil, err + } + defer resp.Close() + jsonData, err := ioutil.ReadAll(resp) + if err != nil { + return false, resp, err + } + var result bool + if err := json.Unmarshal(jsonData, &result); err != nil { + return false, resp, NewError(StatusInvalidResponse, map[string]interface{}{ + "method": "json.Unmarshal", + "error": err.Error(), + }) + } + return result, resp, nil +} diff --git a/v2/db_test.go b/v2/db_test.go new file mode 100644 index 0000000..ce22d55 --- /dev/null +++ b/v2/db_test.go @@ -0,0 +1,82 @@ +package grnci + +import ( + "log" + "testing" +) + +func TestDBColumnRemove(t *testing.T) { + client, err := NewHTTPClient("", nil) + if err != nil { + t.Skipf("NewHTTPClient failed: %v", err) + } + db := NewDB(client) + defer db.Close() + + result, resp, err := db.ColumnRemove("no_such_table", "no_such_column") + if err != nil { + t.Fatalf("db.ColumnRemove failed: %v", err) + } + log.Printf("result = %#v", result) + log.Printf("resp = %#v", resp) + if err := resp.Err(); err != nil { + log.Printf("error = %#v", err) + } +} + +func TestDBStatus(t *testing.T) { + client, err := NewHTTPClient("", nil) + if err != nil { + t.Skipf("NewHTTPClient failed: %v", err) + } + db := NewDB(client) + defer db.Close() + + result, resp, err := db.Status() + if err != nil { + t.Fatalf("db.Status failed: %v", err) + } + log.Printf("result = %#v", result) + log.Printf("resp = %#v", resp) + if err := resp.Err(); err != nil { + log.Printf("error = %#v", err) + } +} + +func TestDBTruncate(t *testing.T) { + client, err := NewHTTPClient("", nil) + if err != nil { + t.Skipf("NewHTTPClient failed: %v", err) + } + db := NewDB(client) + defer db.Close() + + result, resp, err := db.Truncate("no_such_target") + if err != nil { + t.Fatalf("db.Truncate failed: %v", err) + } + log.Printf("result = %#v", result) + log.Printf("resp = %#v", resp) + if err := resp.Err(); err != nil { + log.Printf("error = %#v", err) + } +} + +func TestDBTableRemove(t *testing.T) { + client, err := NewHTTPClient("", nil) + if err != nil { + t.Skipf("NewHTTPClient failed: %v", err) + } + db := NewDB(client) + defer db.Close() + + result, resp, err := db.TableRemove("no_such_table", false) + if err != nil { + t.Fatalf("db.TableRemove failed: %v", err) + } + log.Printf("result = %#v", result) + log.Printf("resp = %#v", resp) + if err := resp.Err(); err != nil { + log.Printf("error = %#v", err) + } +} diff --git a/v2/gqtp.go b/v2/gqtp.go index 6173c25..c83aa83 100644 --- a/v2/gqtp.go +++ b/v2/gqtp.go @@ -378,7 +378,16 @@ func (c *GQTPConn) Exec(cmd string, body io.Reader) (Response, error) { return c.execBody(cmd, body) } -// Query sends a request and receives a response. +// Invoke assembles cmd, params and body into a Request and calls Query. +func (c *GQTPConn) Invoke(cmd string, params map[string]interface{}, body io.Reader) (Response, error) { + req, err := NewRequest(cmd, params, body) + if err != nil { + return nil, err + } + return c.Query(req) +} + +// Query calls Exec with req.GQTPRequest and returns the result. func (c *GQTPConn) Query(req *Request) (Response, error) { cmd, body, err := req.GQTPRequest() if err != nil { @@ -451,6 +460,15 @@ func (c *GQTPClient) Exec(cmd string, body io.Reader) (Response, error) { return resp, nil } +// Invoke assembles cmd, params and body into a Request and calls Query. +func (c *GQTPClient) Invoke(cmd string, params map[string]interface{}, body io.Reader) (Response, error) { + req, err := NewRequest(cmd, params, body) + if err != nil { + return nil, err + } + return c.Query(req) +} + // Query calls Exec with req.GQTPRequest and returns the result. func (c *GQTPClient) Query(req *Request) (Response, error) { cmd, body, err := req.GQTPRequest() diff --git a/v2/gqtp_test.go b/v2/gqtp_test.go index 1d9080b..e334f64 100644 --- a/v2/gqtp_test.go +++ b/v2/gqtp_test.go @@ -101,3 +101,17 @@ func TestGQTPClient(t *testing.T) { } } } + +func TestGQTPConnHandler(t *testing.T) { + var i interface{} = &GQTPConn{} + if _, ok := i.(Handler); !ok { + t.Fatalf("Failed to cast from *GQTPConn to Handler") + } +} + +func TestGQTPClientHandler(t *testing.T) { + var i interface{} = &GQTPClient{} + if _, ok := i.(Handler); !ok { + t.Fatalf("Failed to cast from *GQTPClient to Handler") + } +} diff --git a/v2/handler.go b/v2/handler.go new file mode 100644 index 0000000..0ecc56d --- /dev/null +++ b/v2/handler.go @@ -0,0 +1,11 @@ +package grnci + +import "io" + +// Handler defines the required methods of DB clients and handles. +type Handler interface { + Exec(cmd string, body io.Reader) (Response, error) + Invoke(cmd string, params map[string]interface{}, body io.Reader) (Response, error) + Query(req *Request) (Response, error) + Close() error +} diff --git a/v2/http.go b/v2/http.go index 9c5e32d..fd81667 100644 --- a/v2/http.go +++ b/v2/http.go @@ -363,22 +363,35 @@ func (c *HTTPClient) exec(cmd string, params map[string]string, body io.Reader) return resp, nil } -// Exec sends a command and returns a response. -// It is the caller's responsibility to close the response. -func (c *HTTPClient) Exec(cmd string, params map[string]string, body io.Reader) (Response, error) { - start := time.Now() - resp, err := c.exec(cmd, params, body) +// Exec assembles cmd and body into a Request and calls Query. +func (c *HTTPClient) Exec(cmd string, body io.Reader) (Response, error) { + req, err := ParseRequest(cmd, body) if err != nil { return nil, err } - return newHTTPResponse(resp, start) + return c.Query(req) +} + +// Invoke assembles cmd, params and body into a Request and calls Query. +func (c *HTTPClient) Invoke(cmd string, params map[string]interface{}, body io.Reader) (Response, error) { + req, err := NewRequest(cmd, params, body) + if err != nil { + return nil, err + } + return c.Query(req) } -// Query calls Exec with req.HTTPRequest and returns the result. +// Query sends a request and receives a response. +// It is the caller's responsibility to close the response. func (c *HTTPClient) Query(req *Request) (Response, error) { + start := time.Now() cmd, params, body, err := req.HTTPRequest() if err != nil { return nil, err } - return c.Exec(cmd, params, body) + resp, err := c.exec(cmd, params, body) + if err != nil { + return nil, err + } + return newHTTPResponse(resp, start) } diff --git a/v2/http_test.go b/v2/http_test.go index eca1bfd..dd016d1 100644 --- a/v2/http_test.go +++ b/v2/http_test.go @@ -37,12 +37,8 @@ func TestHTTPClient(t *testing.T) { if pair.Body != "" { body = strings.NewReader(pair.Body) } - req, err := ParseRequest(pair.Command, body) - if err != nil { - t.Fatalf("ParseRequest failed: %v", err) - } log.Printf("command = %s", pair.Command) - resp, err := client.Query(req) + resp, err := client.Exec(pair.Command, body) if err != nil { t.Fatalf("conn.Exec failed: %v", err) } @@ -58,3 +54,10 @@ func TestHTTPClient(t *testing.T) { } } } + +func TestHTTPClientHandler(t *testing.T) { + var i interface{} = &HTTPClient{} + if _, ok := i.(Handler); !ok { + t.Fatalf("Failed to cast from *HTTPClient to Handler") + } +} diff --git a/v2/libgrn/client.go b/v2/libgrn/client.go index eddaa5c..91b8baf 100644 --- a/v2/libgrn/client.go +++ b/v2/libgrn/client.go @@ -3,7 +3,7 @@ package libgrn import ( "io" - "github.com/groonga/grnci/v2" + "github.com/s-yata/grnci" ) const ( @@ -111,6 +111,15 @@ func (c *Client) Exec(cmd string, body io.Reader) (grnci.Response, error) { return resp, nil } +// Invoke assembles cmd, params and body into a grnci.Request and calls Query. +func (c *Client) Invoke(cmd string, params map[string]interface{}, body io.Reader) (grnci.Response, error) { + req, err := grnci.NewRequest(cmd, params, body) + if err != nil { + return nil, err + } + return c.Query(req) +} + // Query calls Exec with req.GQTPRequest and returns the result. func (c *Client) Query(req *grnci.Request) (grnci.Response, error) { cmd, body, err := req.GQTPRequest() diff --git a/v2/libgrn/client_test.go b/v2/libgrn/client_test.go index 74b93a6..bd0cf9d 100644 --- a/v2/libgrn/client_test.go +++ b/v2/libgrn/client_test.go @@ -6,6 +6,8 @@ import ( "log" "strings" "testing" + + "github.com/s-yata/grnci" ) func TestClientGQTP(t *testing.T) { @@ -101,3 +103,10 @@ func TestClientDB(t *testing.T) { } } } + +func TestClientHandler(t *testing.T) { + var i interface{} = &Client{} + if _, ok := i.(grnci.Handler); !ok { + t.Fatalf("Failed to cast from *Client to grnci.Handler") + } +} diff --git a/v2/libgrn/conn.go b/v2/libgrn/conn.go index f7413de..6a7de6d 100644 --- a/v2/libgrn/conn.go +++ b/v2/libgrn/conn.go @@ -10,7 +10,7 @@ import ( "time" "unsafe" - "github.com/groonga/grnci/v2" + "github.com/s-yata/grnci" ) const ( @@ -284,6 +284,15 @@ func (c *Conn) Exec(cmd string, body io.Reader) (grnci.Response, error) { return c.execBody(cmd, body) } +// Invoke assembles cmd, params and body into a grnci.Request and calls Query. +func (c *Conn) Invoke(cmd string, params map[string]interface{}, body io.Reader) (grnci.Response, error) { + req, err := grnci.NewRequest(cmd, params, body) + if err != nil { + return nil, err + } + return c.Query(req) +} + // Query calls Exec with req.GQTPRequest and returns the result. func (c *Conn) Query(req *grnci.Request) (grnci.Response, error) { cmd, body, err := req.GQTPRequest() diff --git a/v2/libgrn/conn_test.go b/v2/libgrn/conn_test.go index fd8d110..f47739d 100644 --- a/v2/libgrn/conn_test.go +++ b/v2/libgrn/conn_test.go @@ -6,6 +6,8 @@ import ( "log" "strings" "testing" + + "github.com/s-yata/grnci" ) func TestConnGQTP(t *testing.T) { @@ -101,3 +103,10 @@ func TestConnDB(t *testing.T) { } } } + +func TestConnHandler(t *testing.T) { + var i interface{} = &Conn{} + if _, ok := i.(grnci.Handler); !ok { + t.Fatalf("Failed to cast from *Conn to grnci.Handler") + } +} diff --git a/v2/libgrn/libgrn.go b/v2/libgrn/libgrn.go index 07e3edf..1cbfc9f 100644 --- a/v2/libgrn/libgrn.go +++ b/v2/libgrn/libgrn.go @@ -10,7 +10,7 @@ import ( "sync" "unsafe" - "github.com/groonga/grnci/v2" + "github.com/s-yata/grnci" ) const ( diff --git a/v2/libgrn/response.go b/v2/libgrn/response.go index f73e353..9cceb47 100644 --- a/v2/libgrn/response.go +++ b/v2/libgrn/response.go @@ -5,7 +5,7 @@ import ( "io/ioutil" "time" - "github.com/groonga/grnci/v2" + "github.com/s-yata/grnci" ) // response is a response. diff --git a/v2/request.go b/v2/request.go index c984c0e..7488815 100644 --- a/v2/request.go +++ b/v2/request.go @@ -3,7 +3,9 @@ package grnci import ( "fmt" "io" + "reflect" "sort" + "strconv" "strings" ) @@ -16,27 +18,30 @@ type Request struct { Body io.Reader // Body (nil is allowed) } +// newRequest returns a new Request with empty Params. +func newRequest(cmd string, body io.Reader) *Request { + return &Request{ + Command: cmd, + CommandRule: GetCommandRule(cmd), + Params: make(map[string]string), + Body: body, + } +} + // NewRequest returns a new Request. -func NewRequest(cmd string, params map[string]string, body io.Reader) (*Request, error) { +func NewRequest(cmd string, params map[string]interface{}, body io.Reader) (*Request, error) { if err := checkCommand(cmd); err != nil { return nil, err } - cr := GetCommandRule(cmd) - paramsCopy := make(map[string]string) + r := newRequest(cmd, body) for k, v := range params { - if err := cr.CheckParam(k, v); err != nil { + if err := r.AddParam(k, v); err != nil { return nil, EnhanceError(err, map[string]interface{}{ "command": cmd, }) } - paramsCopy[k] = v } - return &Request{ - Command: cmd, - CommandRule: cr, - Params: paramsCopy, - Body: body, - }, nil + return r, nil } // unescapeCommandByte returns an unescaped byte. @@ -120,12 +125,7 @@ func ParseRequest(cmd string, body io.Reader) (*Request, error) { if err := checkCommand(tokens[0]); err != nil { return nil, err } - cr := GetCommandRule(tokens[0]) - r := &Request{ - Command: tokens[0], - CommandRule: cr, - Body: body, - } + r := newRequest(tokens[0], body) for i := 1; i < len(tokens); i++ { var k, v string if strings.HasPrefix(tokens[i], "--") { @@ -144,9 +144,33 @@ func ParseRequest(cmd string, body io.Reader) (*Request, error) { return r, nil } +// convertParamValue converts a parameter value. +func (r *Request) convertParamValue(k string, v interface{}) (string, error) { + if v == nil { + return "null", nil + } + val := reflect.ValueOf(v) + switch val.Kind() { + case reflect.Bool: + return strconv.FormatBool(val.Bool()), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(val.Int(), 10), nil + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return strconv.FormatUint(val.Uint(), 10), nil + case reflect.String: + return val.String(), nil + default: + return "", NewError(StatusInvalidCommand, map[string]interface{}{ + "key": k, + "value": v, + "error": "The value type is not supported.", + }) + } +} + // AddParam adds a parameter. // AddParam assumes that Command is already set. -func (r *Request) AddParam(key, value string) error { +func (r *Request) AddParam(key string, value interface{}) error { if r.CommandRule == nil { r.CommandRule = GetCommandRule(r.Command) } @@ -165,10 +189,16 @@ func (r *Request) AddParam(key, value string) error { "key": key, }) } + v, err := r.convertParamValue(pr.Key, value) + if err != nil { + return EnhanceError(err, map[string]interface{}{ + "command": r.Command, + }) + } if r.Params == nil { r.Params = make(map[string]string) } - r.Params[pr.Key] = value + r.Params[pr.Key] = v r.NAnonParams++ return nil } @@ -177,10 +207,28 @@ func (r *Request) AddParam(key, value string) error { "command": r.Command, }) } + v, err := r.convertParamValue(key, value) + if err != nil { + return EnhanceError(err, map[string]interface{}{ + "command": r.Command, + }) + } if r.Params == nil { r.Params = make(map[string]string) } - r.Params[key] = value + r.Params[key] = v + return nil +} + +// RemoveParam removes a parameter. +func (r *Request) RemoveParam(key string) error { + if _, ok := r.Params[key]; !ok { + return NewError(StatusInvalidOperation, map[string]interface{}{ + "key": key, + "error": "The key does not exist.", + }) + } + delete(r.Params, key) return nil } @@ -213,7 +261,7 @@ func (r *Request) GQTPRequest() (cmd string, body io.Reader, err error) { for i := 0; i < len(v); i++ { switch v[i] { case '\'', '\\', '\b', '\t', '\r', '\n': - buf = append(buf, '\'') + buf = append(buf, '\\') } buf = append(buf, v[i]) } diff --git a/v2/request_test.go b/v2/request_test.go index 813843a..9b3f9ee 100644 --- a/v2/request_test.go +++ b/v2/request_test.go @@ -1,14 +1,17 @@ package grnci import ( + "fmt" "testing" ) func TestNewRequest(t *testing.T) { - params := map[string]string{ + params := map[string]interface{}{ "table": "Tbl", "filter": "value < 100", "sort_keys": "value", + "offset": 0, + "limit": -1, } req, err := NewRequest("select", params, nil) if err != nil { @@ -19,8 +22,8 @@ func TestNewRequest(t *testing.T) { req.Command, "select") } for key, value := range params { - if req.Params[key] != value { - t.Fatalf("ParseRequest failed: params[\"%s\"] = %s, want = %s", + if req.Params[key] != fmt.Sprint(value) { + t.Fatalf("ParseRequest failed: params[\"%s\"] = %s, want = %v", key, req.Params[key], value) } } @@ -50,10 +53,12 @@ func TestParseRequest(t *testing.T) { } func TestRequestAddParam(t *testing.T) { - params := map[string]string{ + params := map[string]interface{}{ "table": "Tbl", "filter": "value < 100", "sort_keys": "value", + "offset": 0, + "limit": -1, } req, err := NewRequest("select", nil, nil) if err != nil { @@ -65,17 +70,51 @@ func TestRequestAddParam(t *testing.T) { } } if req.Command != "select" { - t.Fatalf("ParseRequest failed: cmd = %s, want = %s", + t.Fatalf("req.AddParam failed: cmd = %s, want = %s", req.Command, "select") } for key, value := range params { - if req.Params[key] != value { - t.Fatalf("ParseRequest failed: params[\"%s\"] = %s, want = %s", + if req.Params[key] != fmt.Sprint(value) { + t.Fatalf("req.AddParam failed: params[\"%s\"] = %s, want = %v", key, req.Params[key], value) } } } +func TestRequestRemoveParam(t *testing.T) { + params := map[string]interface{}{ + "table": "Tbl", + "filter": "value < 100", + "sort_keys": "value", + "offset": 0, + "limit": -1, + } + req, err := NewRequest("select", nil, nil) + if err != nil { + t.Fatalf("NewRequest failed: %v", err) + } + for key, value := range params { + if err := req.AddParam(key, value); err != nil { + t.Fatalf("req.AddParam failed: %v", err) + } + } + for key := range params { + if err := req.RemoveParam(key); err != nil { + t.Fatalf("req.RemoveParam failed: %v", err) + } + } + if req.Command != "select" { + t.Fatalf("req.RemoveParam failed: cmd = %s, want = %s", + req.Command, "select") + } + for key := range params { + if _, ok := req.Params[key]; ok { + t.Fatalf("req.RemoveParam failed: params[\"%s\"] = %s", + key, req.Params[key]) + } + } +} + func TestRequestCheck(t *testing.T) { data := map[string]bool{ "status": true, @@ -100,10 +139,12 @@ func TestRequestCheck(t *testing.T) { } func TestRequestGQTPRequest(t *testing.T) { - params := map[string]string{ + params := map[string]interface{}{ "table": "Tbl", "filter": "value < 100", "sort_keys": "value", + "offset": 0, + "limit": -1, } req, err := NewRequest("select", params, nil) if err != nil { @@ -113,7 +154,7 @@ func TestRequestGQTPRequest(t *testing.T) { if err != nil { t.Fatalf("req.GQTPRequest failed: %v", err) } - want := "select --filter 'value < 100' --sort_keys 'value' --table 'Tbl'" + want := "select --filter 'value < 100' --limit '-1' --offset '0' --sort_keys 'value' --table 'Tbl'" if actual != want { t.Fatalf("req.GQTPRequest failed: actual = %s, want = %s", actual, want) diff --git a/v2/rule.go b/v2/rule.go index 3496045..ccbaf93 100644 --- a/v2/rule.go +++ b/v2/rule.go @@ -1,5 +1,9 @@ package grnci +import ( + "reflect" +) + // TODO: add functions to check parameters. // checkParamKeyDefault is the default function to check parameter keys. @@ -30,12 +34,27 @@ func checkParamKeyDefault(k string) error { } // checkParamValueDefault is the default function to check parameter values. -func checkParamValueDefault(v string) error { +func checkParamValueDefault(v interface{}) error { + if v == nil { + return nil + } + val := reflect.ValueOf(v) + switch val.Kind() { + case reflect.Bool: + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + case reflect.String: + default: + return NewError(StatusInvalidCommand, map[string]interface{}{ + "value": v, + "error": "The value type is not supported.", + }) + } return nil } // checkParamDefault is the default function to check parameters. -func checkParamDefault(k, v string) error { +func checkParamDefault(k string, v interface{}) error { if err := checkParamKeyDefault(k); err != nil { return EnhanceError(err, map[string]interface{}{ "value": v, @@ -73,13 +92,13 @@ func checkCommand(s string) error { // ParamRule is a parameter rule. type ParamRule struct { - Key string // Parameter key - ValueChecker func(v string) error // Function to check parameter values - Required bool // Whether the parameter is required + Key string // Parameter key + ValueChecker func(v interface{}) error // Function to check parameter values + Required bool // Whether the parameter is required } // NewParamRule returns a new ParamRule. -func NewParamRule(key string, valueChecker func(v string) error, required bool) *ParamRule { +func NewParamRule(key string, valueChecker func(v interface{}) error, required bool) *ParamRule { return &ParamRule{ Key: key, ValueChecker: valueChecker, @@ -88,7 +107,7 @@ func NewParamRule(key string, valueChecker func(v string) error, required bool) } // CheckValue checks a parameter value. -func (pr *ParamRule) CheckValue(v string) error { +func (pr *ParamRule) CheckValue(v interface{}) error { if pr.ValueChecker != nil { return pr.ValueChecker(v) } @@ -97,9 +116,9 @@ func (pr *ParamRule) CheckValue(v string) error { // CommandRule is a command rule. type CommandRule struct { - ParamChecker func(k, v string) error // Function to check uncommon parameters - ParamRules []*ParamRule // Ordered common parameters - ParamRulesMap map[string]*ParamRule // Index for ParamRules + ParamChecker func(k string, v interface{}) error // Function to check uncommon parameters + ParamRules []*ParamRule // Ordered common parameters + ParamRulesMap map[string]*ParamRule // Index for ParamRules } // GetCommandRule returns the command rule for the specified command. @@ -111,7 +130,7 @@ func GetCommandRule(cmd string) *CommandRule { } // NewCommandRule returns a new CommandRule. -func NewCommandRule(paramChecker func(k, v string) error, prs ...*ParamRule) *CommandRule { +func NewCommandRule(paramChecker func(k string, v interface{}) error, prs ...*ParamRule) *CommandRule { prMap := make(map[string]*ParamRule) for _, pr := range prs { prMap[pr.Key] = pr @@ -124,7 +143,7 @@ func NewCommandRule(paramChecker func(k, v string) error, prs ...*ParamRule) *Co } // CheckParam checks a parameter. -func (cr *CommandRule) CheckParam(k, v string) error { +func (cr *CommandRule) CheckParam(k string, v interface{}) error { if cr, ok := cr.ParamRulesMap[k]; ok { if err := cr.CheckValue(v); err != nil { return EnhanceError(err, map[string]interface{}{