-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsession.go
More file actions
157 lines (131 loc) · 3.88 KB
/
Copy pathsession.go
File metadata and controls
157 lines (131 loc) · 3.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package sqlok
import (
"database/sql"
"errors"
"fmt"
"reflect"
"strings"
)
var ErrIdentityConflict = errors.New("identity map conflict: another object with the same ID already exists in the session")
// Session represents the Unit of Work. It tracks object states and
// manages the identity of entities in memory.
type Session struct {
// db is the underlying SQL database connection.
db *sql.DB
// identityMap ensures that only one instance of an entity exists in memory.
// Structure: [reflect.Type][PrimaryKey] -> *ObjectPointer
identityMap map[reflect.Type]map[any]any
// snapshots stores the field-level hashes for each tracked object.
// Structure: *ObjectPointer -> map[ColumnName]Hash (uint32)
snapshots map[any]map[string]uint32
// pending holds new objects that have been Added but not yet Inserted into the DB.
pending []any
}
// NewSession initializes a new Unit of Work with empty maps.
func NewSession(db *sql.DB) *Session {
return &Session{
db: db,
identityMap: make(map[reflect.Type]map[any]any),
snapshots: make(map[any]map[string]uint32),
}
}
// Add registers an entity into the session's identity map.
// If the entity has no primary key, it is added to the pending queue for INSERT.
func (s *Session) Add(ent any) error {
v := reflect.ValueOf(ent)
if v.Kind() != reflect.Ptr {
return errors.New("only pointers to structs can be added to session")
}
t := v.Type().Elem()
id := s.getPrimaryKey(ent)
if id != nil {
if s.identityMap[t] == nil {
s.identityMap[t] = make(map[any]any)
}
if existing, ok := s.identityMap[t][id]; ok {
if existing != ent {
// TODO: Future - implement merge strategy here
return ErrIdentityConflict
}
return nil // Object already tracked, skipping.
}
// Register the pointer
s.identityMap[t][id] = ent
// Take the initial "snapshot" for dirty checking later
// s.takeSnapshot(ent)
return nil
}
// No ID? It's a new entity, queue for Flush -> INSERT
s.pending = append(s.pending, ent)
return nil
}
// Load retrieves an entity of type T by its primary key from the session's identity map.
// If the entity is not found in the session, it returns nil, nil for now.
func Load[T any](s *Session, id any) (*T, error) {
var t T
reflectType := reflect.TypeOf(t)
// Check the Identity Map first
if typeMap, ok := s.identityMap[reflectType]; ok {
if existing, found := typeMap[id]; found {
return existing.(*T), nil
}
}
// TODO: Future - Database lookup using Mapper and Builder
return nil, nil
}
// getPrimaryKey scans for all fields tagged with 'pk' and returns a single or composite identity.
func (s *Session) getPrimaryKey(ent any) any {
v := reflect.ValueOf(ent).Elem()
pks := s.collectPKs(v)
if len(pks) == 0 {
return nil
}
// Simple PK: Return the single value (int, string, etc.)
if len(pks) == 1 {
return pks[0]
}
// Composite PK: Build a unique string key for the identity map.
var sb strings.Builder
sb.WriteString("composite:")
for i, pk := range pks {
if i > 0 {
sb.WriteString("|")
}
sb.WriteString(fmt.Sprintf("%v", pk))
}
return sb.String()
}
// collectPKs recursively gathers all field values marked with 'pk'.
func (s *Session) collectPKs(v reflect.Value) []any {
var pks []any
t := v.Type()
for i := range t.NumField() {
field := t.Field(i)
fieldVal := v.Field(i)
// Support for embedded structs (Composition)
if field.Anonymous && field.Type.Kind() == reflect.Struct {
pks = append(pks, s.collectPKs(fieldVal)...)
continue
}
if tag := field.Tag.Get("sqlok"); tag == "pk" {
val := s.extractValue(fieldVal)
if val != nil {
pks = append(pks, val)
}
}
}
return pks
}
// extractValue handles pointer vs value logic for PK fields.
func (s *Session) extractValue(v reflect.Value) any {
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil
}
return v.Elem().Interface()
}
if v.IsZero() {
return nil
}
return v.Interface()
}