@an5/adapters 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +189 -304
  2. package/dist/an5Adapter.d.ts +85 -7
  3. package/dist/an5Adapter.js +736 -64
  4. package/dist/base/metadata.d.ts +10 -0
  5. package/dist/base/metadata.js +107 -0
  6. package/dist/base/sql.d.ts +19 -1
  7. package/dist/base/sql.js +152 -24
  8. package/dist/base/types.d.ts +4 -1
  9. package/dist/browser.d.ts +5 -2
  10. package/dist/browser.js +10 -2
  11. package/dist/googlesheets/adapter.d.ts +1 -0
  12. package/dist/googlesheets/adapter.js +9 -2
  13. package/dist/googlesheets/config.d.ts +4 -4
  14. package/dist/googlesheets/helpers.js +2 -0
  15. package/dist/googlesheets/parseConnectionString.js +5 -2
  16. package/dist/googlesheets/sqlExecutor.js +29 -24
  17. package/dist/googlesheets/tableClient.js +14 -5
  18. package/dist/index.d.ts +5 -1
  19. package/dist/index.js +12 -1
  20. package/dist/mssql/engine.js +8 -4
  21. package/dist/mysql/engine.js +1 -1
  22. package/dist/postgres/engine.js +1 -1
  23. package/dist/sqlite/browserEngine.d.ts +27 -0
  24. package/dist/sqlite/browserEngine.js +161 -0
  25. package/dist/sqlite/engine.js +15 -3
  26. package/dist/sqlite/index.d.ts +1 -0
  27. package/dist/sqlite/index.js +1 -0
  28. package/dist/typed.d.ts +5 -0
  29. package/dist/typed.js +2 -0
  30. package/dotnet/An5Adapter.cs +1089 -275
  31. package/dotnet/Base/Core.cs +340 -82
  32. package/dotnet/Mssql/MssqlEngine.cs +145 -144
  33. package/dotnet/Postgres/PostgresEngine.cs +121 -121
  34. package/golang/an5_adapter.go +314 -0
  35. package/golang/base/base.go +100 -0
  36. package/golang/base/metadata.go +116 -0
  37. package/golang/base/where.go +311 -0
  38. package/golang/go.mod +3 -0
  39. package/golang/mssql/mssql.go +32 -0
  40. package/golang/postgres/postgres.go +37 -0
  41. package/golang/sqlite/sqlite.go +14 -0
  42. package/golang/sqlite_test.go +42 -0
  43. package/golang/table_client.go +1329 -0
  44. package/package.json +18 -8
  45. package/python/an5_adapter.py +119 -97
  46. package/python/base/__init__.py +3 -3
  47. package/python/base/dialects.py +11 -8
  48. package/python/base/metadata.py +35 -10
  49. package/python/base/sql.py +172 -121
  50. package/python/mssql/__init__.py +1 -1
  51. package/python/mssql/provider.py +62 -62
  52. package/python/postgres/__init__.py +1 -1
  53. package/python/postgres/provider.py +15 -15
  54. package/python/table_client.py +619 -216
  55. package/typescript/src/an5Adapter.ts +1220 -505
  56. package/typescript/src/base/index.ts +3 -3
  57. package/typescript/src/base/metadata.ts +148 -28
  58. package/typescript/src/base/sql.ts +264 -98
  59. package/typescript/src/base/types.ts +27 -24
  60. package/typescript/src/browser.ts +10 -6
  61. package/typescript/src/config.ts +95 -95
  62. package/typescript/src/googlesheets/adapter.ts +269 -259
  63. package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
  64. package/typescript/src/googlesheets/config.ts +47 -47
  65. package/typescript/src/googlesheets/helpers.ts +144 -143
  66. package/typescript/src/googlesheets/index.ts +3 -3
  67. package/typescript/src/googlesheets/parseConnectionString.ts +48 -46
  68. package/typescript/src/googlesheets/retry.ts +126 -126
  69. package/typescript/src/googlesheets/sqlExecutor.ts +304 -301
  70. package/typescript/src/googlesheets/tableClient.ts +461 -455
  71. package/typescript/src/googlesheets/types.ts +6 -6
  72. package/typescript/src/index.ts +32 -19
  73. package/typescript/src/mssql/engine.ts +134 -132
  74. package/typescript/src/mssql/index.ts +1 -1
  75. package/typescript/src/mysql/engine.ts +80 -80
  76. package/typescript/src/mysql/index.ts +1 -1
  77. package/typescript/src/postgres/engine.ts +87 -87
  78. package/typescript/src/postgres/index.ts +1 -1
  79. package/typescript/src/sqlite/browserEngine.ts +192 -0
  80. package/typescript/src/sqlite/engine.ts +66 -55
  81. package/typescript/src/sqlite/index.ts +3 -1
  82. package/typescript/src/typed.ts +52 -0
@@ -0,0 +1,314 @@
1
+ // Package an5adapters provides standalone database adapter engines for Go applications.
2
+ package an5adapters
3
+
4
+ import (
5
+ "context"
6
+ "database/sql"
7
+ "encoding/json"
8
+ "sort"
9
+ "strings"
10
+
11
+ "an5adapters/base"
12
+ "an5adapters/mssql"
13
+ "an5adapters/postgres"
14
+ "an5adapters/sqlite"
15
+ )
16
+
17
+ // Re-export core base types and functions for package root convenience.
18
+ type Dialect = base.Dialect
19
+ type AdapterMetadata = base.AdapterMetadata
20
+ type RelationDef = base.RelationDef
21
+
22
+ const (
23
+ DialectMssql = base.DialectMssql
24
+ DialectPostgres = base.DialectPostgres
25
+ DialectSqlite = base.DialectSqlite
26
+ )
27
+
28
+ func DetectDialect(connStr string) Dialect {
29
+ return base.DetectDialect(connStr)
30
+ }
31
+
32
+ func QuoteIdentifier(name string, dialect Dialect) string {
33
+ return base.QuoteIdentifier(name, dialect)
34
+ }
35
+
36
+ func QuoteTable(tableName string, dialect Dialect) string {
37
+ return base.QuoteTable(tableName, dialect)
38
+ }
39
+
40
+ func CosineSimilarity(v1, v2 []float64) float64 {
41
+ return base.CosineSimilarity(v1, v2)
42
+ }
43
+
44
+ func EuclideanDistance(v1, v2 []float64) float64 {
45
+ return base.EuclideanDistance(v1, v2)
46
+ }
47
+
48
+ func DotProduct(v1, v2 []float64) float64 {
49
+ return base.DotProduct(v1, v2)
50
+ }
51
+
52
+ // SetAdapterMetadata injects model-to-table metadata from the generated client.
53
+ func SetAdapterMetadata(meta AdapterMetadata) {
54
+ base.SetAdapterMetadata(meta)
55
+ }
56
+
57
+ // GetModelToTable returns the current model-to-table mapping.
58
+ func GetModelToTable() map[string]string {
59
+ return base.GetModelToTable()
60
+ }
61
+
62
+ // GetRelationsForModel returns the relation definitions for the given model name.
63
+ func GetRelationsForModel(modelName string) map[string]RelationDef {
64
+ return base.GetRelationsForModel(modelName)
65
+ }
66
+
67
+ // An5Adapter manages runtime database execution for Go applications.
68
+ type An5Adapter struct {
69
+ DB *sql.DB
70
+ Dialect Dialect
71
+ }
72
+
73
+ // NewAn5Adapter constructs an An5Adapter instance.
74
+ func NewAn5Adapter(db *sql.DB, connStr string) *An5Adapter {
75
+ return &An5Adapter{
76
+ DB: db,
77
+ Dialect: base.DetectDialect(connStr),
78
+ }
79
+ }
80
+
81
+ // Connect validates the database connection (ping).
82
+ func (a *An5Adapter) Connect(ctx context.Context) error {
83
+ return a.DB.PingContext(ctx)
84
+ }
85
+
86
+ // Disconnect closes the underlying database connection pool.
87
+ func (a *An5Adapter) Disconnect() error {
88
+ return a.DB.Close()
89
+ }
90
+
91
+ // QueryRaw executes a SELECT query and returns rows as maps.
92
+ func (a *An5Adapter) QueryRaw(ctx context.Context, query string, args ...interface{}) ([]map[string]interface{}, error) {
93
+ rows, err := a.DB.QueryContext(ctx, query, args...)
94
+ if err != nil {
95
+ return nil, err
96
+ }
97
+ defer rows.Close()
98
+
99
+ cols, err := rows.Columns()
100
+ if err != nil {
101
+ return nil, err
102
+ }
103
+
104
+ var results []map[string]interface{}
105
+ for rows.Next() {
106
+ columns := make([]interface{}, len(cols))
107
+ columnPointers := make([]interface{}, len(cols))
108
+ for i := range columns {
109
+ columnPointers[i] = &columns[i]
110
+ }
111
+
112
+ if err := rows.Scan(columnPointers...); err != nil {
113
+ return nil, err
114
+ }
115
+
116
+ m := make(map[string]interface{})
117
+ for i, colName := range cols {
118
+ val := columnPointers[i].(*interface{})
119
+ m[colName] = *val
120
+ }
121
+ results = append(results, m)
122
+ }
123
+ if err := rows.Err(); err != nil {
124
+ return nil, err
125
+ }
126
+ return results, nil
127
+ }
128
+
129
+ // ExecuteRaw executes an INSERT, UPDATE, or DELETE query and returns affected rows count.
130
+ func (a *An5Adapter) ExecuteRaw(ctx context.Context, query string, args ...interface{}) (int64, error) {
131
+ res, err := a.DB.ExecContext(ctx, query, args...)
132
+ if err != nil {
133
+ return 0, err
134
+ }
135
+ return res.RowsAffected()
136
+ }
137
+
138
+ // Transaction executes a function within a database transaction.
139
+ func (a *An5Adapter) Transaction(ctx context.Context, fn func(tx *sql.Tx) error) error {
140
+ tx, err := a.DB.BeginTx(ctx, nil)
141
+ if err != nil {
142
+ return err
143
+ }
144
+ defer func() {
145
+ if p := recover(); p != nil {
146
+ _ = tx.Rollback()
147
+ panic(p)
148
+ }
149
+ }()
150
+
151
+ if err := fn(tx); err != nil {
152
+ _ = tx.Rollback()
153
+ return err
154
+ }
155
+ return tx.Commit()
156
+ }
157
+
158
+ // VectorSearch ranks rows by vector distance using native SQL execution with in-memory fallback.
159
+ func (a *An5Adapter) VectorSearch(ctx context.Context, tableName string, targetVector []float64, take int, whereClause string, vectorField string, distanceMetric string, args ...interface{}) ([]map[string]interface{}, error) {
160
+ if vectorField == "" {
161
+ vectorField = "embedding"
162
+ }
163
+ if distanceMetric == "" {
164
+ distanceMetric = "cosine"
165
+ }
166
+ dim := len(targetVector)
167
+ vecBytes, _ := json.Marshal(targetVector)
168
+ vecStr := string(vecBytes)
169
+
170
+ // 1. Primary path: Native database SQL vector query execution via dialect provider (mssql / postgres / sqlite)
171
+ var sqlQuery string
172
+ if a.Dialect == base.DialectPostgres {
173
+ sqlQuery = postgres.BuildVectorSearchQuery(tableName, vectorField, distanceMetric, dim, take, whereClause)
174
+ } else if a.Dialect == base.DialectMssql {
175
+ sqlQuery = mssql.BuildVectorSearchQuery(tableName, vectorField, distanceMetric, dim, take, whereClause)
176
+ } else if a.Dialect == base.DialectSqlite {
177
+ sqlQuery = sqlite.BuildVectorSearchQuery(tableName, vectorField, distanceMetric, dim, take, whereClause)
178
+ }
179
+
180
+ if sqlQuery != "" {
181
+ queryArgs := append([]interface{}{vecStr}, args...)
182
+ nativeRows, err := a.QueryRaw(ctx, sqlQuery, queryArgs...)
183
+ if err == nil {
184
+ return nativeRows, nil
185
+ }
186
+ }
187
+
188
+ // 2. Secondary fallback: Fetch rows and compute vector distance in-memory if DB lacks native vector extensions
189
+ fallbackQuery := "SELECT * FROM " + base.QuoteTable(tableName, a.Dialect)
190
+ if strings.TrimSpace(whereClause) != "" {
191
+ fallbackQuery += " WHERE " + whereClause
192
+ }
193
+
194
+ rows, err := a.QueryRaw(ctx, fallbackQuery, args...)
195
+ if err != nil {
196
+ return nil, err
197
+ }
198
+
199
+ return VectorSearchFallback(rows, targetVector, take, vectorField, distanceMetric), nil
200
+ }
201
+
202
+ // VectorSearchFallback ranks rows in-memory.
203
+ func VectorSearchFallback(rows []map[string]interface{}, targetVector []float64, take int, vectorField string, distanceMetric string) []map[string]interface{} {
204
+ type scored struct {
205
+ row map[string]interface{}
206
+ distance float64
207
+ }
208
+ var list []scored
209
+
210
+ for _, r := range rows {
211
+ raw, ok := r[vectorField]
212
+ if !ok || raw == nil {
213
+ continue
214
+ }
215
+
216
+ var rowVec []float64
217
+ switch v := raw.(type) {
218
+ case string:
219
+ _ = json.Unmarshal([]byte(v), &rowVec)
220
+ case []byte:
221
+ _ = json.Unmarshal(v, &rowVec)
222
+ case []float64:
223
+ rowVec = v
224
+ case []interface{}:
225
+ for _, item := range v {
226
+ if f, ok := item.(float64); ok {
227
+ rowVec = append(rowVec, f)
228
+ }
229
+ }
230
+ }
231
+
232
+ if len(rowVec) != len(targetVector) {
233
+ continue
234
+ }
235
+
236
+ var dist float64
237
+ switch strings.ToLower(distanceMetric) {
238
+ case "euclidean":
239
+ dist = base.EuclideanDistance(targetVector, rowVec)
240
+ case "dot":
241
+ dist = -base.DotProduct(targetVector, rowVec)
242
+ default: // cosine
243
+ dist = 1.0 - base.CosineSimilarity(targetVector, rowVec)
244
+ }
245
+
246
+ rCopy := make(map[string]interface{})
247
+ for k, val := range r {
248
+ rCopy[k] = val
249
+ }
250
+ rCopy["distance"] = dist
251
+ list = append(list, scored{row: rCopy, distance: dist})
252
+ }
253
+
254
+ sort.Slice(list, func(i, j int) bool {
255
+ return list[i].distance < list[j].distance
256
+ })
257
+
258
+ var result []map[string]interface{}
259
+ limit := take
260
+ if limit <= 0 || limit > len(list) {
261
+ limit = len(list)
262
+ }
263
+ for i := 0; i < limit; i++ {
264
+ result = append(result, list[i].row)
265
+ }
266
+ return result
267
+ }
268
+
269
+ // QueryProc executes a stored procedure and returns the result rows.
270
+ func (a *An5Adapter) QueryProc(ctx context.Context, procName string, args ...interface{}) ([]map[string]interface{}, error) {
271
+ var sql string
272
+ if a.Dialect == base.DialectPostgres {
273
+ placeholders := make([]string, len(args))
274
+ for i := range args {
275
+ placeholders[i] = placeholder(a.Dialect, i+1)
276
+ }
277
+ sql = "CALL " + procName + "(" + strings.Join(placeholders, ", ") + ")"
278
+ } else {
279
+ placeholders := make([]string, len(args))
280
+ for i := range args {
281
+ placeholders[i] = "?"
282
+ }
283
+ if len(placeholders) > 0 {
284
+ sql = "EXEC " + procName + " " + strings.Join(placeholders, ", ")
285
+ } else {
286
+ sql = "EXEC " + procName
287
+ }
288
+ }
289
+ return a.QueryRaw(ctx, sql, args...)
290
+ }
291
+
292
+ // ExecuteProc executes a stored procedure and returns affected rows.
293
+ func (a *An5Adapter) ExecuteProc(ctx context.Context, procName string, args ...interface{}) (int64, error) {
294
+ var sql string
295
+ if a.Dialect == base.DialectPostgres {
296
+ placeholders := make([]string, len(args))
297
+ for i := range args {
298
+ placeholders[i] = placeholder(a.Dialect, i+1)
299
+ }
300
+ sql = "CALL " + procName + "(" + strings.Join(placeholders, ", ") + ")"
301
+ } else {
302
+ placeholders := make([]string, len(args))
303
+ for i := range args {
304
+ placeholders[i] = "?"
305
+ }
306
+ if len(placeholders) > 0 {
307
+ sql = "EXEC " + procName + " " + strings.Join(placeholders, ", ")
308
+ } else {
309
+ sql = "EXEC " + procName
310
+ }
311
+ }
312
+ return a.ExecuteRaw(ctx, sql, args...)
313
+ }
314
+
@@ -0,0 +1,100 @@
1
+ // Package base provides core dialect types, quoting utilities, and vector math for AN5 Go Adapters.
2
+ package base
3
+
4
+ import (
5
+ "math"
6
+ "strings"
7
+ )
8
+
9
+ // Dialect represents supported database engines.
10
+ type Dialect string
11
+
12
+ const (
13
+ DialectMssql Dialect = "mssql"
14
+ DialectPostgres Dialect = "postgres"
15
+ DialectSqlite Dialect = "sqlite"
16
+ )
17
+
18
+ // DetectDialect parses connection string to determine database dialect.
19
+ func DetectDialect(connStr string) Dialect {
20
+ lower := strings.ToLower(strings.TrimSpace(connStr))
21
+ if strings.HasPrefix(lower, "postgres://") || strings.HasPrefix(lower, "postgresql://") || strings.Contains(lower, "port=5432") {
22
+ return DialectPostgres
23
+ }
24
+ if strings.HasPrefix(lower, "sqlite://") || strings.HasPrefix(lower, "sqlite:") || strings.HasPrefix(lower, "file:") || strings.HasSuffix(lower, ".db") || strings.HasSuffix(lower, ".sqlite") || strings.HasSuffix(lower, ".sqlite3") || lower == ":memory:" {
25
+ return DialectSqlite
26
+ }
27
+ return DialectMssql
28
+ }
29
+
30
+ // QuoteIdentifier quotes column or identifier names according to dialect.
31
+ func QuoteIdentifier(name string, dialect Dialect) string {
32
+ raw := stripWrapping(name, "[", "]")
33
+ if dialect == DialectPostgres || dialect == DialectSqlite {
34
+ raw = stripWrapping(raw, `"`, `"`)
35
+ return `"` + strings.ReplaceAll(raw, `"`, `""`) + `"`
36
+ }
37
+ return "[" + strings.ReplaceAll(raw, "]", "]]") + "]"
38
+ }
39
+
40
+ // QuoteTable quotes schema-qualified table names (e.g. "dbo.users" -> "[dbo].[users]").
41
+ func QuoteTable(tableName string, dialect Dialect) string {
42
+ if strings.HasPrefix(tableName, "[") || strings.HasPrefix(tableName, `"`) {
43
+ return tableName
44
+ }
45
+ parts := strings.Split(tableName, ".")
46
+ var quoted []string
47
+ for _, p := range parts {
48
+ quoted = append(quoted, QuoteIdentifier(p, dialect))
49
+ }
50
+ return strings.Join(quoted, ".")
51
+ }
52
+
53
+ func stripWrapping(name, left, right string) string {
54
+ if strings.HasPrefix(name, left) && strings.HasSuffix(name, right) {
55
+ return strings.TrimSuffix(strings.TrimPrefix(name, left), right)
56
+ }
57
+ return name
58
+ }
59
+
60
+ // CosineSimilarity computes cosine similarity between two float64 vectors.
61
+ func CosineSimilarity(v1, v2 []float64) float64 {
62
+ if len(v1) != len(v2) || len(v1) == 0 {
63
+ return 0
64
+ }
65
+ var dot, m1, m2 float64
66
+ for i := 0; i < len(v1); i++ {
67
+ dot += v1[i] * v2[i]
68
+ m1 += v1[i] * v1[i]
69
+ m2 += v2[i] * v2[i]
70
+ }
71
+ if m1 == 0 || m2 == 0 {
72
+ return 0
73
+ }
74
+ return dot / (math.Sqrt(m1) * math.Sqrt(m2))
75
+ }
76
+
77
+ // EuclideanDistance computes L2 distance between two float64 vectors.
78
+ func EuclideanDistance(v1, v2 []float64) float64 {
79
+ if len(v1) != len(v2) || len(v1) == 0 {
80
+ return 0
81
+ }
82
+ var sum float64
83
+ for i := 0; i < len(v1); i++ {
84
+ diff := v1[i] - v2[i]
85
+ sum += diff * diff
86
+ }
87
+ return math.Sqrt(sum)
88
+ }
89
+
90
+ // DotProduct computes inner product of two float64 vectors.
91
+ func DotProduct(v1, v2 []float64) float64 {
92
+ if len(v1) != len(v2) || len(v1) == 0 {
93
+ return 0
94
+ }
95
+ var dot float64
96
+ for i := 0; i < len(v1); i++ {
97
+ dot += v1[i] * v2[i]
98
+ }
99
+ return dot
100
+ }
@@ -0,0 +1,116 @@
1
+ // Package base provides core dialect types, quoting utilities, vector math,
2
+ // and adapter metadata for AN5 Go Adapters.
3
+ package base
4
+
5
+ import (
6
+ "strings"
7
+ "sync"
8
+ )
9
+
10
+ // RelationDef describes a model relation for eager-loading (include) support.
11
+ type RelationDef struct {
12
+ ModelName string
13
+ RelationType string // "many" | "one"
14
+ ForeignKey string
15
+ LocalKey string
16
+ }
17
+
18
+ // AdapterMetadata holds model-to-table, field mapping, and relation mapping injected by generated clients.
19
+ type AdapterMetadata struct {
20
+ ModelToTable map[string]string
21
+ ModelFields map[string]interface{}
22
+ RelationMap map[string]map[string]RelationDef
23
+ }
24
+
25
+ var (
26
+ metaMu sync.RWMutex
27
+ modelToTable = map[string]string{}
28
+ modelFields = map[string]interface{}{}
29
+ relationMap = map[string]map[string]RelationDef{}
30
+ )
31
+
32
+ // SetAdapterMetadata stores metadata provided by the generated an5Client.
33
+ // Call this once at application startup after initializing the DB context.
34
+ func SetAdapterMetadata(meta AdapterMetadata) {
35
+ metaMu.Lock()
36
+ defer metaMu.Unlock()
37
+ modelToTable = make(map[string]string, len(meta.ModelToTable))
38
+ for k, v := range meta.ModelToTable {
39
+ modelToTable[k] = v
40
+ }
41
+ modelFields = make(map[string]interface{}, len(meta.ModelFields))
42
+ for k, v := range meta.ModelFields {
43
+ modelFields[k] = v
44
+ }
45
+ relationMap = make(map[string]map[string]RelationDef, len(meta.RelationMap))
46
+ for model, rels := range meta.RelationMap {
47
+ relationMap[model] = make(map[string]RelationDef, len(rels))
48
+ for key, def := range rels {
49
+ relationMap[model][key] = def
50
+ }
51
+ }
52
+ }
53
+
54
+ // GetModelToTable returns the model-to-table mapping (read-only snapshot).
55
+ func GetModelToTable() map[string]string {
56
+ metaMu.RLock()
57
+ defer metaMu.RUnlock()
58
+ out := make(map[string]string, len(modelToTable))
59
+ for k, v := range modelToTable {
60
+ out[k] = v
61
+ }
62
+ return out
63
+ }
64
+
65
+ // GetFieldsForModel returns field metadata for the given model name.
66
+ func GetFieldsForModel(modelName string) interface{} {
67
+ metaMu.RLock()
68
+ defer metaMu.RUnlock()
69
+ return modelFields[modelName]
70
+ }
71
+
72
+ // GetRelationsForModel returns the relation map for the given model name (read-only snapshot).
73
+ func GetRelationsForModel(modelName string) map[string]RelationDef {
74
+ metaMu.RLock()
75
+ defer metaMu.RUnlock()
76
+ rels := relationMap[modelName]
77
+ if rels == nil {
78
+ return map[string]RelationDef{}
79
+ }
80
+ out := make(map[string]RelationDef, len(rels))
81
+ for k, v := range rels {
82
+ out[k] = v
83
+ }
84
+ return out
85
+ }
86
+
87
+ // GetRelationMap returns the full model-to-relations map (read-only snapshot).
88
+ func GetRelationMap() map[string]map[string]RelationDef {
89
+ metaMu.RLock()
90
+ defer metaMu.RUnlock()
91
+ out := make(map[string]map[string]RelationDef, len(relationMap))
92
+ for model, rels := range relationMap {
93
+ out[model] = make(map[string]RelationDef, len(rels))
94
+ for k, v := range rels {
95
+ out[model][k] = v
96
+ }
97
+ }
98
+ return out
99
+ }
100
+
101
+ // ResolveTable looks up the physical table name for a model name (case-insensitive fallback).
102
+ func ResolveTable(modelName string) string {
103
+ metaMu.RLock()
104
+ defer metaMu.RUnlock()
105
+ if t, ok := modelToTable[modelName]; ok {
106
+ return t
107
+ }
108
+ // lowercase fallback
109
+ lower := strings.ToLower(modelName)
110
+ for k, v := range modelToTable {
111
+ if strings.ToLower(k) == lower {
112
+ return v
113
+ }
114
+ }
115
+ return modelName
116
+ }