fix: 将 libsql-client-go 作为普通目录提交,而非 gitlink

This commit is contained in:
2026-04-27 07:04:46 +08:00
parent 2359d6c9fa
commit f6332fbaaf
52 changed files with 42333 additions and 1 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, condition *BatchCondition) {
b.Steps = append(b.Steps, BatchStep{Stmt: stmt, Condition: condition})
}

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,58 @@
package hrana
import (
"encoding/json"
"fmt"
"reflect"
"testing"
)
func TestBatchResult_UnmarshalJSON(t *testing.T) {
testCases := []struct {
name string
jsonData []byte
expected *uint64
}{
{
jsonData: []byte(`{"replication_index":1}`),
expected: uint64Ptr(1),
},
{
jsonData: []byte(`{"replication_index":"1"}`),
expected: uint64Ptr(1),
},
{
jsonData: []byte(`{"replication_index":""}`),
expected: nil,
},
{
jsonData: []byte(`{}`),
expected: nil,
},
{
jsonData: []byte(`{"replication_index":"0"}`),
expected: uint64Ptr(0),
},
{
jsonData: []byte(`{"replication_index":0}`),
expected: uint64Ptr(0),
},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
batchResult := &BatchResult{}
err := json.Unmarshal(tc.jsonData, batchResult)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(batchResult.ReplicationIndex, tc.expected) {
t.Errorf("ReplicationIndex field is not correctly unmarshaled got = %v, want = %v", batchResult.ReplicationIndex, tc.expected)
}
})
}
}
func uint64Ptr(n uint64) *uint64 {
return &n
}

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,54 @@
package hrana
import (
"encoding/json"
"fmt"
"reflect"
"testing"
)
func TestStmtResult_UnmarshalJSON(t *testing.T) {
testCases := []struct {
name string
jsonData []byte
expected *uint64
}{
{
jsonData: []byte(`{"replication_index":1}`),
expected: uint64Ptr(1),
},
{
jsonData: []byte(`{"replication_index":"1"}`),
expected: uint64Ptr(1),
},
{
jsonData: []byte(`{"replication_index":""}`),
expected: nil,
},
{
jsonData: []byte(`{}`),
expected: nil,
},
{
jsonData: []byte(`{"replication_index":"0"}`),
expected: uint64Ptr(0),
},
{
jsonData: []byte(`{"replication_index":0}`),
expected: uint64Ptr(0),
},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
stmtResult := &StmtResult{}
err := json.Unmarshal(tc.jsonData, stmtResult)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(stmtResult.ReplicationIndex, tc.expected) {
t.Errorf("ReplicationIndex field is not correctly unmarshaled got = %v, want = %v", stmtResult.ReplicationIndex, tc.expected)
}
})
}
}

View File

@@ -0,0 +1,98 @@
package hrana
import (
"reflect"
"testing"
)
func TestStmtWithPositionalArgs(t *testing.T) {
tests := []struct {
name string
args []any
want []Value
wantErr bool
}{
{
name: "int args",
args: []any{1, 2},
want: []Value{{Type: "integer", Value: "1"}, {Type: "integer", Value: "2"}},
},
{
name: "string args",
args: []any{"a", "b"},
want: []Value{{Type: "text", Value: "a"}, {Type: "text", Value: "b"}},
},
{
name: "invalid arg",
args: []any{make(chan int)},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt := Stmt{}
err := stmt.AddPositionalArgs(tt.args)
if (err != nil) != tt.wantErr {
t.Errorf("StmtWithPositionalArgs() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(stmt.Args, tt.want) {
t.Errorf("got = %v, want %v", stmt.Args, tt.want)
}
})
}
}
func TestStmtWithNamedArgs(t *testing.T) {
tests := []struct {
name string
args map[string]any
want []NamedArg
wantErr bool
}{
{
name: "int args",
args: map[string]any{"arg1": 1, "arg2": int64(2)},
want: []NamedArg{
{Name: "arg1", Value: Value{Type: "integer", Value: "1"}},
{Name: "arg2", Value: Value{Type: "integer", Value: "2"}},
},
},
{
name: "string args",
args: map[string]any{"arg1": "a", "arg2": "b"},
want: []NamedArg{
{Name: "arg1", Value: Value{Type: "text", Value: "a"}},
{Name: "arg2", Value: Value{Type: "text", Value: "b"}},
},
},
{
name: "invalid arg",
args: map[string]any{"arg1": make(chan int)},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt := Stmt{}
err := stmt.AddNamedArgs(tt.args)
if (err != nil) != tt.wantErr {
t.Errorf("StmtWithNamedArgs() error = %v, wantErr %v", err, tt.wantErr)
return
}
got := make(map[NamedArg]struct{})
want := make(map[NamedArg]struct{})
for _, arg := range stmt.NamedArgs {
got[arg] = struct{}{}
}
for _, arg := range tt.want {
want[arg] = struct{}{}
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", stmt.NamedArgs, tt.want)
}
})
}
}

View File

@@ -0,0 +1,88 @@
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 params != nil {
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, transactional bool) (*StreamRequest, error) {
size := len(sqls)
if transactional {
size += 1
}
batch := &Batch{Steps: make([]BatchStep, 0, size)}
addArgs := len(params) > 0
for idx, sql := range sqls {
s := sql
stmt := &Stmt{
Sql: &s,
WantRows: wantRows,
}
if addArgs {
if err := stmt.AddArgs(params[idx]); err != nil {
return nil, err
}
}
var condition *BatchCondition
if transactional {
if idx > 0 {
prev_idx := int32(idx - 1)
condition = &BatchCondition{
Type: "ok",
Step: &prev_idx,
}
}
}
batch.Add(*stmt, condition)
}
if transactional {
rollback := "ROLLBACK"
last_idx := int32(len(sqls) - 1)
batch.Add(Stmt{Sql: &rollback, WantRows: false},
&BatchCondition{Type: "not", Cond: &BatchCondition{Type: "ok", Step: &last_idx}})
}
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,89 @@
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" {
if v.Base64 == nil {
return nil
}
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"
b64 := base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString(blob)
res.Base64 = &b64
} 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
}

View File

@@ -0,0 +1,304 @@
package hrana
import (
"encoding/json"
"reflect"
"strconv"
"testing"
"time"
)
func toPtr[T any](v T) *T {
return &v
}
func TestValueToValue(t *testing.T) {
tests := []struct {
name string
columnType string
value Value
want any
}{
{
name: "null",
value: Value{
Type: "null",
Value: nil,
},
want: nil,
},
{
name: "int",
value: Value{
Type: "integer",
Value: strconv.FormatInt(int64(42), 10),
},
want: int64(42),
},
{
name: "string",
value: Value{
Type: "text",
Value: "foo",
},
want: "foo",
},
{
name: "bytes",
value: Value{
Type: "blob",
Base64: toPtr("YmFy"),
},
want: []byte("bar"),
},
{
name: "bytes",
value: Value{
Type: "blob",
Base64: toPtr(""),
},
want: []byte{},
},
{
name: "float",
value: Value{
Type: "float",
Value: 3.14,
},
want: 3.14,
},
{
name: "timestamp",
columnType: "TIMESTAMP",
value: Value{
Type: "text",
Value: "0001-01-01 01:00:00+00:00",
},
want: time.Time{}.Add(time.Hour),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var columnType *string = nil
if tt.columnType != "" {
columnType = &tt.columnType
}
got := tt.value.ToValue(columnType)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToValue() = %v, want %v", got, tt.want)
}
})
}
}
func TestToValue(t *testing.T) {
tests := []struct {
name string
value any
want Value
wantErr bool
}{
{
name: "null",
value: nil,
want: Value{
Type: "null",
},
},
{
name: "int",
value: 42,
want: Value{
Type: "integer",
Value: strconv.FormatInt(int64(42), 10),
},
},
{
name: "string",
value: "foo",
want: Value{
Type: "text",
Value: "foo",
},
},
{
name: "bytes",
value: []byte{},
want: Value{
Type: "blob",
Base64: toPtr(""),
},
},
{
name: "float",
value: 3.14,
want: Value{
Type: "float",
Value: 3.14,
},
},
{
name: "boolean",
value: true,
want: Value{
Type: "integer",
Value: "1",
},
},
{
name: "timestamp",
value: time.Time{}.Add(time.Hour),
want: Value{
Type: "text",
Value: "0001-01-01 01:00:00+00:00",
},
},
{
name: "unsupported",
value: make(chan int),
want: Value{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ToValue(tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("ToValue() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToValue() = %v, want %v", got, tt.want)
}
})
}
}
func TestMarshal(t *testing.T) {
tests := []struct {
name string
value Value
marshaled string
}{
{
name: "null",
value: Value{
Type: "null",
},
marshaled: `{"type":"null"}`,
},
{
name: "int",
value: Value{
Type: "integer",
Value: strconv.FormatInt(int64(42), 10),
},
marshaled: `{"type":"integer","value":"42"}`,
},
{
name: "string",
value: Value{
Type: "text",
Value: "foo",
},
marshaled: `{"type":"text","value":"foo"}`,
},
{
name: "bytes",
value: Value{
Type: "blob",
Base64: toPtr("YmFy"),
},
marshaled: `{"type":"blob","base64":"YmFy"}`,
},
{
name: "float",
value: Value{
Type: "float",
Value: 3.14,
},
marshaled: `{"type":"float","value":3.14}`,
},
{
name: "timestamp",
value: Value{
Type: "text",
Value: time.Time{}.Add(time.Hour),
},
marshaled: `{"type":"text","value":"0001-01-01T01:00:00Z"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := json.Marshal(tt.value)
if err != nil {
t.Errorf("json.Marshal() error = %v", err)
return
}
if !reflect.DeepEqual(string(got), tt.marshaled) {
t.Errorf("json.Marshal() = %v, want %v", string(got), tt.marshaled)
}
})
}
}
func TestUnmarshal(t *testing.T) {
tests := []struct {
name string
value Value
marshaled string
}{
{
name: "null",
value: Value{
Type: "null",
},
marshaled: `{"type":"null"}`,
},
{
name: "int",
value: Value{
Type: "integer",
Value: strconv.FormatInt(int64(42), 10),
},
marshaled: `{"type":"integer","value":"42"}`,
},
{
name: "string",
value: Value{
Type: "text",
Value: "foo",
},
marshaled: `{"type":"text","value":"foo"}`,
},
{
name: "bytes",
value: Value{
Type: "blob",
Base64: toPtr("YmFy"),
},
marshaled: `{"type":"blob","base64":"YmFy"}`,
},
{
name: "float",
value: Value{
Type: "float",
Value: 3.14,
},
marshaled: `{"type":"float","value":3.14}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got Value
err := json.Unmarshal([]byte(tt.marshaled), &got)
if err != nil {
t.Errorf("json.Marshal() error = %v", err)
return
}
if !reflect.DeepEqual(got, tt.value) {
t.Errorf("json.Unmarshal() = %v, want %v", got, tt.value)
}
})
}
}