docs: Add CLAUDE.md with codebase guidance
Create comprehensive documentation for future Claude Code instances working in this repository, including: - Development commands for testing, building, and code quality - Core architecture overview of the SQL query builder system - Directory structure and component explanations - Testing patterns and conventions - Key dependencies and their purposes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+54
-6
@@ -5,12 +5,13 @@ import (
|
||||
gsql "database/sql"
|
||||
"reflect"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/internal/errors"
|
||||
"git.fsdpf.net/go/db/v2/internal/util"
|
||||
"git.fsdpf.net/go/db/internal/errors"
|
||||
"git.fsdpf.net/go/db/internal/util"
|
||||
)
|
||||
|
||||
type (
|
||||
QueryExecutor struct {
|
||||
hook func(result interface{}) error
|
||||
de DbExecutor
|
||||
err error
|
||||
query string
|
||||
@@ -19,6 +20,8 @@ type (
|
||||
)
|
||||
|
||||
var (
|
||||
errUnsupportedScanMapType = errors.New("type must be a pointer to a map when scanning into a map")
|
||||
errUnsupportedScanMapsType = errors.New("type must be a pointer to a slice when scanning into a map")
|
||||
errUnsupportedScanStructType = errors.New("type must be a pointer to a struct when scanning into a struct")
|
||||
errUnsupportedScanStructsType = errors.New("type must be a pointer to a slice when scanning into structs")
|
||||
errUnsupportedScanValsType = errors.New("type must be a pointer to a slice when scanning into vals")
|
||||
@@ -38,22 +41,34 @@ func (q QueryExecutor) Exec() (gsql.Result, error) {
|
||||
return q.ExecContext(context.Background())
|
||||
}
|
||||
|
||||
func (q QueryExecutor) ExecContext(ctx context.Context) (gsql.Result, error) {
|
||||
func (q QueryExecutor) ExecContext(ctx context.Context) (result gsql.Result, err error) {
|
||||
if q.err != nil {
|
||||
return nil, q.err
|
||||
}
|
||||
return q.de.ExecContext(ctx, q.query, q.args...)
|
||||
result, err = q.de.ExecContext(ctx, q.query, q.args...)
|
||||
defer func() {
|
||||
if err == nil && q.hook != nil {
|
||||
err = q.hook(result)
|
||||
}
|
||||
}()
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (q QueryExecutor) Query() (*gsql.Rows, error) {
|
||||
return q.QueryContext(context.Background())
|
||||
}
|
||||
|
||||
func (q QueryExecutor) QueryContext(ctx context.Context) (*gsql.Rows, error) {
|
||||
func (q QueryExecutor) QueryContext(ctx context.Context) (result *gsql.Rows, err error) {
|
||||
if q.err != nil {
|
||||
return nil, q.err
|
||||
}
|
||||
return q.de.QueryContext(ctx, q.query, q.args...)
|
||||
result, err = q.de.QueryContext(ctx, q.query, q.args...)
|
||||
defer func() {
|
||||
if err == nil && q.hook != nil {
|
||||
err = q.hook(result)
|
||||
}
|
||||
}()
|
||||
return result, err
|
||||
}
|
||||
|
||||
// This will execute the SQL and append results to the slice
|
||||
@@ -238,6 +253,35 @@ func (q QueryExecutor) ScanValContext(ctx context.Context, i interface{}) (bool,
|
||||
return false, scanner.Err()
|
||||
}
|
||||
|
||||
func (q QueryExecutor) GetRecord() (map[string]any, error) {
|
||||
return q.GetRecordContext(context.Background())
|
||||
}
|
||||
|
||||
func (q QueryExecutor) GetRecordContext(ctx context.Context) (map[string]any, error) {
|
||||
scanner, err := q.ScannerContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = scanner.Close() }()
|
||||
if scanner.Next() {
|
||||
return scanner.GetRecord()
|
||||
}
|
||||
return nil, scanner.Err()
|
||||
}
|
||||
|
||||
func (q QueryExecutor) GetRecords() ([]map[string]any, error) {
|
||||
return q.GetRecordsContext(context.Background())
|
||||
}
|
||||
|
||||
func (q QueryExecutor) GetRecordsContext(ctx context.Context) ([]map[string]any, error) {
|
||||
scanner, err := q.ScannerContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = scanner.Close() }()
|
||||
return scanner.GetRecords()
|
||||
}
|
||||
|
||||
// Scanner will return a Scanner that can be used for manually scanning rows.
|
||||
func (q QueryExecutor) Scanner() (Scanner, error) {
|
||||
return q.ScannerContext(context.Background())
|
||||
@@ -251,3 +295,7 @@ func (q QueryExecutor) ScannerContext(ctx context.Context) (Scanner, error) {
|
||||
}
|
||||
return NewScanner(rows), nil
|
||||
}
|
||||
|
||||
func (q *QueryExecutor) Hook(hook func(dataset interface{}) error) {
|
||||
q.hook = hook
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
"git.fsdpf.net/go/db/internal/sb"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package exec
|
||||
|
||||
// Hooks 钩子实例
|
||||
type Hooks interface {
|
||||
Before(dataset interface{}) error
|
||||
After(dataset interface{}, result interface{}) error
|
||||
}
|
||||
+132
-21
@@ -2,11 +2,11 @@ package exec
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/errors"
|
||||
"git.fsdpf.net/go/db/v2/internal/util"
|
||||
"git.fsdpf.net/go/db/internal/errors"
|
||||
"git.fsdpf.net/go/db/internal/util"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -17,6 +17,8 @@ type (
|
||||
ScanStructs(i interface{}) error
|
||||
ScanVal(i interface{}) error
|
||||
ScanVals(i interface{}) error
|
||||
GetRecord() (map[string]any, error)
|
||||
GetRecords() ([]map[string]any, error)
|
||||
Close() error
|
||||
Err() error
|
||||
}
|
||||
@@ -49,6 +51,86 @@ func (s *scanner) Err() error {
|
||||
return s.rows.Err()
|
||||
}
|
||||
|
||||
func (s *scanner) GetRecords() ([]map[string]any, error) {
|
||||
records := []map[string]any{}
|
||||
for s.Next() {
|
||||
row, err := s.GetRecord()
|
||||
if err != nil {
|
||||
return records, err
|
||||
}
|
||||
records = append(records, row)
|
||||
}
|
||||
|
||||
return records, s.Err()
|
||||
}
|
||||
|
||||
func (s *scanner) GetRecord() (record map[string]any, err error) {
|
||||
// Setup columns, but only once.
|
||||
if s.columns == nil || s.columnMap == nil {
|
||||
colsType, err := s.rows.ColumnTypes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.columns = make([]string, len(colsType))
|
||||
s.columnMap = util.ColumnMap{}
|
||||
for i := range colsType {
|
||||
s.columns[i] = colsType[i].Name()
|
||||
|
||||
typ := colsType[i].ScanType()
|
||||
|
||||
// 强制日期时间为字符串
|
||||
if typ == reflect.TypeOf(sql.NullTime{}) {
|
||||
typ = reflect.TypeOf(sql.Null[[]uint8]{})
|
||||
}
|
||||
|
||||
s.columnMap[colsType[i].Name()] = util.ColumnData{
|
||||
ColumnName: colsType[i].Name(),
|
||||
GoType: typ,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scans := make([]interface{}, len(s.columns))
|
||||
|
||||
for i, col := range s.columns {
|
||||
scans[i] = reflect.New(s.columnMap[col].GoType).Interface()
|
||||
}
|
||||
|
||||
if err := s.rows.Scan(scans...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
record = make(map[string]interface{}, len(s.columns))
|
||||
for i, col := range s.columns {
|
||||
var vv any
|
||||
switch v := scans[i].(type) {
|
||||
case *sql.Null[[]uint8]: // 强制日期时间为字符串
|
||||
if vv, err = v.Value(); err == nil && vv != nil {
|
||||
vv = string(vv.([]uint8))
|
||||
}
|
||||
case *sql.RawBytes:
|
||||
if len(*v) > 1 {
|
||||
if rune((*v)[0]) == rune('[') || rune((*v)[0]) == rune('{') {
|
||||
err = json.Unmarshal(*v, &vv)
|
||||
} else {
|
||||
vv = string(*v)
|
||||
}
|
||||
} else {
|
||||
vv = string(*v)
|
||||
}
|
||||
default:
|
||||
vv = reflect.Indirect(reflect.ValueOf(v)).Interface()
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
record[col] = vv
|
||||
}
|
||||
|
||||
return record, s.Err()
|
||||
}
|
||||
|
||||
// ScanStruct will scan the current row into i.
|
||||
func (s *scanner) ScanStruct(i interface{}) error {
|
||||
// Setup columnMap and columns, but only once.
|
||||
@@ -63,35 +145,37 @@ func (s *scanner) ScanStruct(i interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 补全未知字段类型
|
||||
if len(cols) != len(cm) {
|
||||
colTypes, err := s.rows.ColumnTypes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, t := range colTypes {
|
||||
if _, ok := cm[t.Name()]; !ok {
|
||||
cm[t.Name()] = util.ColumnData{
|
||||
ColumnName: t.Name(),
|
||||
GoType: t.ScanType(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.columnMap = cm
|
||||
s.columns = cols
|
||||
}
|
||||
|
||||
scans := make([]interface{}, 0, len(s.columns))
|
||||
for _, col := range s.columns {
|
||||
data, ok := s.columnMap[col]
|
||||
scans, err := createColumnScans(s.columns, s.columnMap)
|
||||
|
||||
if !ok {
|
||||
return unableToFindFieldError(col)
|
||||
}
|
||||
// 处理 converting NULL to string is unsupported
|
||||
// 前面将 string 和 int 类型转为了 *string 和 *int
|
||||
switch data.GoType.Kind() {
|
||||
case reflect.String,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Float32, reflect.Float64,
|
||||
reflect.Bool:
|
||||
scans = append(scans, reflect.New(reflect.PointerTo(data.GoType)).Interface())
|
||||
default:
|
||||
scans = append(scans, reflect.New(data.GoType).Interface())
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.rows.Scan(scans...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := exp.Record{}
|
||||
record := map[string]interface{}{}
|
||||
for index, col := range s.columns {
|
||||
record[col] = scans[index]
|
||||
}
|
||||
@@ -175,3 +259,30 @@ func checkScanValsTarget(i interface{}) (reflect.Value, error) {
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func createColumnScans(cols []string, cm util.ColumnMap) (scans []interface{}, err error) {
|
||||
scans = make([]interface{}, 0, len(cols))
|
||||
|
||||
for _, col := range cols {
|
||||
data, ok := cm[col]
|
||||
|
||||
if !ok {
|
||||
return scans, unableToFindFieldError(col)
|
||||
}
|
||||
// 处理 converting NULL to string is unsupported
|
||||
// 前面将 string 和 int 类型转为了 *string 和 *int
|
||||
switch data.GoType.Kind() {
|
||||
case reflect.String,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Float32, reflect.Float64,
|
||||
reflect.Bool:
|
||||
scans = append(scans, reflect.New(reflect.PointerTo(data.GoType)).Interface())
|
||||
case reflect.Map, reflect.Slice, reflect.Struct:
|
||||
scans = append(scans, reflect.New(reflect.PointerTo(reflect.TypeOf(json.RawMessage{}))).Interface())
|
||||
default:
|
||||
scans = append(scans, reflect.New(data.GoType).Interface())
|
||||
}
|
||||
}
|
||||
|
||||
return scans, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package exec
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/db/exp"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@@ -67,3 +68,27 @@ func (s *scannerSuite) TestScanVals() {
|
||||
s.Require().NoError(err)
|
||||
s.Require().ElementsMatch([]int{1, 2}, result)
|
||||
}
|
||||
|
||||
func (s *scannerSuite) TestGetRecords() {
|
||||
db, mock, err := sqlmock.New()
|
||||
s.Require().NoError(err)
|
||||
|
||||
mock.ExpectQuery("SELECT \\* FROM `items`").
|
||||
WithArgs().
|
||||
WillReturnRows(
|
||||
sqlmock.NewRows([]string{"address", "name"}).
|
||||
AddRow("111 Test Addr", "Test1").
|
||||
AddRow("111 Test Addr", "Test1"),
|
||||
)
|
||||
|
||||
rows, err := db.Query("SELECT \\* FROM `items`")
|
||||
s.Require().NoError(err)
|
||||
|
||||
result, err := NewScanner(rows).GetRecords()
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Equal([]exp.Record{
|
||||
{"address": "111 Test Addr", "name": "Test1"},
|
||||
{"address": "111 Test Addr", "name": "Test1"},
|
||||
}, result)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user