This commit is contained in:
wagslane
2024-03-07 09:48:38 -07:00
parent 2b337f9f0f
commit 86874e2f9e
171 changed files with 63081 additions and 7300 deletions

View File

@@ -0,0 +1,22 @@
package hrana
type Batch struct {
Steps []BatchStep `json:"steps"`
ReplicationIndex *uint64 `json:"replication_index,omitempty"`
}
type BatchStep struct {
Stmt Stmt `json:"stmt"`
Condition *BatchCondition `json:"condition,omitempty"`
}
type BatchCondition struct {
Type string `json:"type"`
Step *int32 `json:"step,omitempty"`
Cond *BatchCondition `json:"cond,omitempty"`
Conds []BatchCondition `json:"conds,omitempty"`
}
func (b *Batch) Add(stmt Stmt) {
b.Steps = append(b.Steps, BatchStep{Stmt: stmt})
}

View File

@@ -0,0 +1,49 @@
package hrana
import (
"encoding/json"
"fmt"
"strconv"
)
type BatchResult struct {
StepResults []*StmtResult `json:"step_results"`
StepErrors []*Error `json:"step_errors"`
ReplicationIndex *uint64 `json:"replication_index"`
}
func (b *BatchResult) UnmarshalJSON(data []byte) error {
type Alias BatchResult
aux := &struct {
ReplicationIndex interface{} `json:"replication_index,omitempty"`
*Alias
}{
Alias: (*Alias)(b),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.ReplicationIndex == nil {
return nil
}
switch v := aux.ReplicationIndex.(type) {
case float64:
repIndex := uint64(v)
b.ReplicationIndex = &repIndex
case string:
if v == "" {
return nil
}
repIndex, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return err
}
b.ReplicationIndex = &repIndex
default:
return fmt.Errorf("invalid type for replication index: %T", v)
}
return nil
}

View File

@@ -0,0 +1,10 @@
package hrana
type PipelineRequest struct {
Baton string `json:"baton,omitempty"`
Requests []StreamRequest `json:"requests"`
}
func (pr *PipelineRequest) Add(request StreamRequest) {
pr.Requests = append(pr.Requests, request)
}

View File

@@ -0,0 +1,7 @@
package hrana
type PipelineResponse struct {
Baton string `json:"baton,omitempty"`
BaseUrl string `json:"base_url,omitempty"`
Results []StreamResult `json:"results"`
}

View File

@@ -0,0 +1,58 @@
package hrana
import (
"github.com/tursodatabase/libsql-client-go/libsql/internal/http/shared"
)
type Stmt struct {
Sql *string `json:"sql,omitempty"`
SqlId *int32 `json:"sql_id,omitempty"`
Args []Value `json:"args,omitempty"`
NamedArgs []NamedArg `json:"named_args,omitempty"`
WantRows bool `json:"want_rows"`
ReplicationIndex *uint64 `json:"replication_index,omitempty"`
}
type NamedArg struct {
Name string `json:"name"`
Value Value `json:"value"`
}
func (s *Stmt) AddArgs(params shared.Params) error {
if len(params.Named()) > 0 {
return s.AddNamedArgs(params.Named())
} else {
return s.AddPositionalArgs(params.Positional())
}
}
func (s *Stmt) AddPositionalArgs(args []any) error {
argValues := make([]Value, len(args))
for idx := range args {
var err error
if argValues[idx], err = ToValue(args[idx]); err != nil {
return err
}
}
s.Args = argValues
return nil
}
func (s *Stmt) AddNamedArgs(args map[string]any) error {
argValues := make([]NamedArg, len(args))
idx := 0
for key, value := range args {
var err error
var v Value
if v, err = ToValue(value); err != nil {
return err
}
argValues[idx] = NamedArg{
Name: key,
Value: v,
}
idx++
}
s.NamedArgs = argValues
return nil
}

View File

@@ -0,0 +1,65 @@
package hrana
import (
"encoding/json"
"fmt"
"strconv"
)
type Column struct {
Name *string `json:"name"`
Type *string `json:"decltype"`
}
type StmtResult struct {
Cols []Column `json:"cols"`
Rows [][]Value `json:"rows"`
AffectedRowCount int32 `json:"affected_row_count"`
LastInsertRowId *string `json:"last_insert_rowid"`
ReplicationIndex *uint64 `json:"replication_index"`
}
func (r *StmtResult) GetLastInsertRowId() int64 {
if r.LastInsertRowId != nil {
if integer, err := strconv.ParseInt(*r.LastInsertRowId, 10, 64); err == nil {
return integer
}
}
return 0
}
func (r *StmtResult) UnmarshalJSON(data []byte) error {
type Alias StmtResult
aux := &struct {
ReplicationIndex interface{} `json:"replication_index,omitempty"`
*Alias
}{
Alias: (*Alias)(r),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.ReplicationIndex == nil {
return nil
}
switch v := aux.ReplicationIndex.(type) {
case float64:
repIndex := uint64(v)
r.ReplicationIndex = &repIndex
case string:
if v == "" {
return nil
}
repIndex, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return err
}
r.ReplicationIndex = &repIndex
default:
return fmt.Errorf("invalid type for replication index: %T", v)
}
return nil
}

View File

@@ -0,0 +1,63 @@
package hrana
import (
"github.com/tursodatabase/libsql-client-go/libsql/internal/http/shared"
)
type StreamRequest struct {
Type string `json:"type"`
Stmt *Stmt `json:"stmt,omitempty"`
Batch *Batch `json:"batch,omitempty"`
Sql *string `json:"sql,omitempty"`
SqlId *int32 `json:"sql_id,omitempty"`
}
func CloseStream() StreamRequest {
return StreamRequest{Type: "close"}
}
func ExecuteStream(sql string, params shared.Params, wantRows bool) (*StreamRequest, error) {
stmt := &Stmt{
Sql: &sql,
WantRows: wantRows,
}
if err := stmt.AddArgs(params); err != nil {
return nil, err
}
return &StreamRequest{Type: "execute", Stmt: stmt}, nil
}
func ExecuteStoredStream(sqlId int32, params shared.Params, wantRows bool) (*StreamRequest, error) {
stmt := &Stmt{
SqlId: &sqlId,
WantRows: wantRows,
}
if err := stmt.AddArgs(params); err != nil {
return nil, err
}
return &StreamRequest{Type: "execute", Stmt: stmt}, nil
}
func BatchStream(sqls []string, params []shared.Params, wantRows bool) (*StreamRequest, error) {
batch := &Batch{}
for idx, sql := range sqls {
s := sql
stmt := &Stmt{
Sql: &s,
WantRows: wantRows,
}
if err := stmt.AddArgs(params[idx]); err != nil {
return nil, err
}
batch.Add(*stmt)
}
return &StreamRequest{Type: "batch", Batch: batch}, nil
}
func StoreSqlStream(sql string, sqlId int32) StreamRequest {
return StreamRequest{Type: "store_sql", Sql: &sql, SqlId: &sqlId}
}
func CloseStoredSqlStream(sqlId int32) StreamRequest {
return StreamRequest{Type: "close_sql", SqlId: &sqlId}
}

View File

@@ -0,0 +1,52 @@
package hrana
import (
"encoding/json"
"errors"
"fmt"
)
type StreamResult struct {
Type string `json:"type"`
Response *StreamResponse `json:"response,omitempty"`
Error *Error `json:"error,omitempty"`
}
type StreamResponse struct {
Type string `json:"type"`
Result json.RawMessage `json:"result,omitempty"`
}
func (r *StreamResponse) ExecuteResult() (*StmtResult, error) {
if r.Type != "execute" {
return nil, fmt.Errorf("invalid response type: %s", r.Type)
}
var res StmtResult
if err := json.Unmarshal(r.Result, &res); err != nil {
return nil, err
}
return &res, nil
}
func (r *StreamResponse) BatchResult() (*BatchResult, error) {
if r.Type != "batch" {
return nil, fmt.Errorf("invalid response type: %s", r.Type)
}
var res BatchResult
if err := json.Unmarshal(r.Result, &res); err != nil {
return nil, err
}
for _, e := range res.StepErrors {
if e != nil {
return nil, errors.New(e.Message)
}
}
return &res, nil
}
type Error struct {
Message string `json:"message"`
Code *string `json:"code,omitempty"`
}

View File

@@ -0,0 +1,85 @@
package hrana
import (
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
)
type Value struct {
Type string `json:"type"`
Value any `json:"value,omitempty"`
Base64 string `json:"base64,omitempty"`
}
func (v Value) ToValue(columnType *string) any {
if v.Type == "blob" {
bytes, err := base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(v.Base64)
if err != nil {
return nil
}
return bytes
} else if v.Type == "integer" {
integer, err := strconv.ParseInt(v.Value.(string), 10, 64)
if err != nil {
return nil
}
return integer
} else if columnType != nil {
if (strings.ToLower(*columnType) == "timestamp" || strings.ToLower(*columnType) == "datetime") && v.Type == "text" {
for _, format := range []string{
"2006-01-02 15:04:05.999999999-07:00",
"2006-01-02T15:04:05.999999999-07:00",
"2006-01-02 15:04:05.999999999",
"2006-01-02T15:04:05.999999999",
"2006-01-02 15:04:05",
"2006-01-02T15:04:05",
"2006-01-02 15:04",
"2006-01-02T15:04",
"2006-01-02",
} {
if t, err := time.ParseInLocation(format, v.Value.(string), time.UTC); err == nil {
return t
}
}
}
}
return v.Value
}
func ToValue(v any) (Value, error) {
var res Value
if v == nil {
res.Type = "null"
} else if integer, ok := v.(int64); ok {
res.Type = "integer"
res.Value = strconv.FormatInt(integer, 10)
} else if integer, ok := v.(int); ok {
res.Type = "integer"
res.Value = strconv.FormatInt(int64(integer), 10)
} else if text, ok := v.(string); ok {
res.Type = "text"
res.Value = text
} else if blob, ok := v.([]byte); ok {
res.Type = "blob"
res.Base64 = base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString(blob)
} else if float, ok := v.(float64); ok {
res.Type = "float"
res.Value = float
} else if t, ok := v.(time.Time); ok {
res.Type = "text"
res.Value = t.Format("2006-01-02 15:04:05.999999999-07:00")
} else if t, ok := v.(bool); ok {
res.Type = "integer"
res.Value = "0"
if t {
res.Value = "1"
}
} else {
return res, fmt.Errorf("unsupported value type: %s", v)
}
return res, nil
}