@an5/adapters 0.2.5 → 0.2.9
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.
- package/README.md +283 -305
- package/dist/an5Adapter.d.ts +142 -7
- package/dist/an5Adapter.js +998 -68
- package/dist/base/metadata.d.ts +10 -0
- package/dist/base/metadata.js +107 -0
- package/dist/base/sql.d.ts +19 -1
- package/dist/base/sql.js +152 -24
- package/dist/base/types.d.ts +10 -1
- package/dist/browser.d.ts +4 -1
- package/dist/browser.js +9 -2
- package/dist/googlesheets/adapter.d.ts +1 -0
- package/dist/googlesheets/adapter.js +9 -2
- package/dist/googlesheets/config.d.ts +4 -4
- package/dist/googlesheets/helpers.js +2 -0
- package/dist/googlesheets/parseConnectionString.js +5 -2
- package/dist/googlesheets/sqlExecutor.js +29 -24
- package/dist/googlesheets/tableClient.js +14 -5
- package/dist/index.d.ts +5 -1
- package/dist/index.js +15 -1
- package/dist/mssql/engine.js +8 -4
- package/dist/mysql/engine.js +1 -1
- package/dist/nbase/client.d.ts +92 -0
- package/dist/nbase/client.js +147 -0
- package/dist/nbase/index.d.ts +47 -0
- package/dist/nbase/index.js +80 -0
- package/dist/postgres/engine.js +1 -1
- package/dist/sqlite/browserEngine.d.ts +27 -0
- package/dist/sqlite/browserEngine.js +161 -0
- package/dist/sqlite/engine.js +15 -3
- package/dist/sqlite/index.d.ts +1 -0
- package/dist/sqlite/index.js +1 -0
- package/dist/typed.d.ts +5 -0
- package/dist/typed.js +2 -0
- package/dotnet/An5Adapter.cs +1112 -275
- package/dotnet/Base/Core.cs +380 -82
- package/dotnet/Mssql/MssqlEngine.cs +145 -144
- package/dotnet/Postgres/PostgresEngine.cs +149 -121
- package/dotnet/Sqlite/SqliteEngine.cs +214 -0
- package/golang/an5_adapter.go +314 -0
- package/golang/base/base.go +100 -0
- package/golang/base/metadata.go +116 -0
- package/golang/base/where.go +311 -0
- package/golang/go.mod +3 -0
- package/golang/mssql/mssql.go +32 -0
- package/golang/postgres/postgres.go +37 -0
- package/golang/sqlite/sqlite.go +14 -0
- package/golang/sqlite_test.go +42 -0
- package/golang/table_client.go +1329 -0
- package/package.json +26 -9
- package/python/an5_adapter.py +184 -97
- package/python/base/__init__.py +3 -3
- package/python/base/dialects.py +11 -8
- package/python/base/metadata.py +59 -10
- package/python/base/sql.py +202 -121
- package/python/mssql/__init__.py +1 -1
- package/python/mssql/provider.py +62 -62
- package/python/postgres/__init__.py +1 -1
- package/python/postgres/provider.py +15 -15
- package/python/sqlite/__init__.py +3 -0
- package/python/sqlite/provider.py +83 -0
- package/python/table_client.py +636 -216
- package/rust/Cargo.toml +31 -0
- package/rust/README.md +71 -0
- package/rust/src/adapter.rs +1312 -0
- package/rust/src/base/dialect.rs +172 -0
- package/rust/src/base/metadata.rs +134 -0
- package/rust/src/base/mod.rs +17 -0
- package/rust/src/base/where_builder.rs +405 -0
- package/rust/src/lib.rs +75 -0
- package/typescript/src/an5Adapter.ts +1515 -505
- package/typescript/src/base/index.ts +3 -3
- package/typescript/src/base/metadata.ts +148 -28
- package/typescript/src/base/sql.ts +264 -98
- package/typescript/src/base/types.ts +33 -24
- package/typescript/src/browser.ts +10 -6
- package/typescript/src/config.ts +95 -95
- package/typescript/src/googlesheets/adapter.ts +269 -259
- package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
- package/typescript/src/googlesheets/config.ts +47 -47
- package/typescript/src/googlesheets/helpers.ts +144 -143
- package/typescript/src/googlesheets/index.ts +3 -3
- package/typescript/src/googlesheets/parseConnectionString.ts +48 -46
- package/typescript/src/googlesheets/retry.ts +126 -126
- package/typescript/src/googlesheets/sqlExecutor.ts +304 -301
- package/typescript/src/googlesheets/tableClient.ts +461 -455
- package/typescript/src/googlesheets/types.ts +6 -6
- package/typescript/src/index.ts +48 -27
- package/typescript/src/mssql/engine.ts +134 -132
- package/typescript/src/mssql/index.ts +1 -1
- package/typescript/src/mysql/engine.ts +80 -80
- package/typescript/src/mysql/index.ts +1 -1
- package/typescript/src/nbase/client.ts +214 -0
- package/typescript/src/nbase/index.ts +104 -0
- package/typescript/src/postgres/engine.ts +87 -87
- package/typescript/src/postgres/index.ts +1 -1
- package/typescript/src/sqlite/browserEngine.ts +192 -0
- package/typescript/src/sqlite/engine.ts +66 -55
- package/typescript/src/sqlite/index.ts +3 -1
- package/typescript/src/typed.ts +52 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Data;
|
|
4
|
+
using System.Reflection;
|
|
5
|
+
|
|
6
|
+
namespace An5Orm
|
|
7
|
+
{
|
|
8
|
+
// ─── SQLite Engine ──────────────────────────────────────────────────────────
|
|
9
|
+
//
|
|
10
|
+
// Microsoft.Data.Sqlite is a different type family from the other two
|
|
11
|
+
// providers (SqliteConnection/SqliteCommand, not DbConnection subclasses you
|
|
12
|
+
// can reuse), so it gets its own engine rather than a shared abstraction.
|
|
13
|
+
//
|
|
14
|
+
// Notable differences from MSSQL, all handled by the SQL builder in
|
|
15
|
+
// An5Adapter rather than here: no WITH (NOLOCK) table hint, no SELECT TOP,
|
|
16
|
+
// LIMIT/OFFSET instead of OFFSET/FETCH NEXT, and no stored procedures.
|
|
17
|
+
|
|
18
|
+
internal class SqliteEngine : IQueryEngine
|
|
19
|
+
{
|
|
20
|
+
public Dialect Dialect => Dialect.Sqlite;
|
|
21
|
+
private readonly string _connectionString;
|
|
22
|
+
private readonly int _commandTimeout;
|
|
23
|
+
|
|
24
|
+
public SqliteEngine(string connectionString, int commandTimeout)
|
|
25
|
+
{
|
|
26
|
+
_connectionString = SqliteConnectionString.Normalize(connectionString);
|
|
27
|
+
_commandTimeout = commandTimeout;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Set while a transaction is open; statements then join it instead of
|
|
31
|
+
// opening a second connection, which SQLite would refuse.
|
|
32
|
+
private Microsoft.Data.Sqlite.SqliteConnection _txnConn;
|
|
33
|
+
|
|
34
|
+
private Microsoft.Data.Sqlite.SqliteConnection OpenNewConnection()
|
|
35
|
+
{
|
|
36
|
+
var conn = new Microsoft.Data.Sqlite.SqliteConnection(_connectionString);
|
|
37
|
+
conn.Open();
|
|
38
|
+
// WAL keeps readers from blocking the writer, and foreign keys are
|
|
39
|
+
// off by default in SQLite even when the schema declares them.
|
|
40
|
+
using var pragma = conn.CreateCommand();
|
|
41
|
+
pragma.CommandText = "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;";
|
|
42
|
+
pragma.ExecuteNonQuery();
|
|
43
|
+
return conn;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private ConnectionScope<Microsoft.Data.Sqlite.SqliteConnection> Acquire()
|
|
47
|
+
=> _txnConn != null
|
|
48
|
+
? new ConnectionScope<Microsoft.Data.Sqlite.SqliteConnection>(_txnConn, owned: false)
|
|
49
|
+
: new ConnectionScope<Microsoft.Data.Sqlite.SqliteConnection>(OpenNewConnection(), owned: true);
|
|
50
|
+
|
|
51
|
+
private Microsoft.Data.Sqlite.SqliteCommand BuildCommand(
|
|
52
|
+
Microsoft.Data.Sqlite.SqliteConnection conn, string sql, Dictionary<string, object> parameters)
|
|
53
|
+
{
|
|
54
|
+
var cmd = conn.CreateCommand();
|
|
55
|
+
cmd.CommandText = sql;
|
|
56
|
+
cmd.CommandTimeout = _commandTimeout;
|
|
57
|
+
if (parameters != null)
|
|
58
|
+
{
|
|
59
|
+
foreach (var kv in parameters)
|
|
60
|
+
{
|
|
61
|
+
// SqliteParameter names must not carry the prefix; the SQL
|
|
62
|
+
// keeps @name and the parameter is registered as name.
|
|
63
|
+
var paramName = kv.Key.TrimStart('@');
|
|
64
|
+
var value = kv.Value ?? DBNull.Value;
|
|
65
|
+
|
|
66
|
+
var param = cmd.CreateParameter();
|
|
67
|
+
param.ParameterName = paramName;
|
|
68
|
+
// A JSON vector or a bool needs a type SQLite will store in a
|
|
69
|
+
// column rather than coerce to an integer.
|
|
70
|
+
if (value is bool flag) param.Value = flag ? 1 : 0;
|
|
71
|
+
else param.Value = value;
|
|
72
|
+
cmd.Parameters.Add(param);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return cmd;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public List<Dictionary<string, object>> QueryRaw(string sql, Dictionary<string, object> parameters)
|
|
79
|
+
{
|
|
80
|
+
var results = new List<Dictionary<string, object>>();
|
|
81
|
+
using var scope = Acquire();
|
|
82
|
+
using var cmd = BuildCommand(scope.Connection, sql, parameters);
|
|
83
|
+
using var reader = cmd.ExecuteReader();
|
|
84
|
+
while (reader.Read())
|
|
85
|
+
{
|
|
86
|
+
var row = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
87
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
88
|
+
{
|
|
89
|
+
row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
|
|
90
|
+
}
|
|
91
|
+
results.Add(row);
|
|
92
|
+
}
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public List<T> QueryRaw<T>(string sql, Dictionary<string, object> parameters) where T : new()
|
|
97
|
+
{
|
|
98
|
+
var results = new List<T>();
|
|
99
|
+
using var scope = Acquire();
|
|
100
|
+
using var cmd = BuildCommand(scope.Connection, sql, parameters);
|
|
101
|
+
using var reader = cmd.ExecuteReader();
|
|
102
|
+
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
103
|
+
while (reader.Read())
|
|
104
|
+
{
|
|
105
|
+
var item = new T();
|
|
106
|
+
foreach (var prop in props)
|
|
107
|
+
{
|
|
108
|
+
if (!HasColumn(reader, prop.Name)) continue;
|
|
109
|
+
var val = reader[prop.Name];
|
|
110
|
+
if (val == DBNull.Value) continue;
|
|
111
|
+
try { prop.SetValue(item, Convert.ChangeType(val, prop.PropertyType)); } catch { }
|
|
112
|
+
}
|
|
113
|
+
results.Add(item);
|
|
114
|
+
}
|
|
115
|
+
return results;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public int ExecuteRaw(string sql, Dictionary<string, object> parameters)
|
|
119
|
+
{
|
|
120
|
+
using var scope = Acquire();
|
|
121
|
+
using var cmd = BuildCommand(scope.Connection, sql, parameters);
|
|
122
|
+
return cmd.ExecuteNonQuery();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private static bool HasColumn(Microsoft.Data.Sqlite.SqliteDataReader reader, string name)
|
|
126
|
+
{
|
|
127
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
128
|
+
if (reader.GetName(i).Equals(name, StringComparison.OrdinalIgnoreCase)) return true;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// <summary>Detaches the transaction connection, so later statements open their own.</summary>
|
|
133
|
+
internal void ReleaseTransactionConnection() => _txnConn = null;
|
|
134
|
+
|
|
135
|
+
public An5TransactionBase BeginTransaction()
|
|
136
|
+
{
|
|
137
|
+
if (_txnConn != null)
|
|
138
|
+
throw new InvalidOperationException("A transaction is already open on this adapter.");
|
|
139
|
+
var conn = OpenNewConnection();
|
|
140
|
+
var tx = conn.BeginTransaction();
|
|
141
|
+
_txnConn = conn;
|
|
142
|
+
return new SqliteTransaction(this, conn, tx);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
internal class SqliteTransaction : An5TransactionBase
|
|
147
|
+
{
|
|
148
|
+
private readonly SqliteEngine _engine;
|
|
149
|
+
private readonly Microsoft.Data.Sqlite.SqliteConnection _conn;
|
|
150
|
+
private readonly Microsoft.Data.Sqlite.SqliteTransaction _tx;
|
|
151
|
+
|
|
152
|
+
public SqliteTransaction(SqliteEngine engine, Microsoft.Data.Sqlite.SqliteConnection conn, Microsoft.Data.Sqlite.SqliteTransaction tx)
|
|
153
|
+
{
|
|
154
|
+
_engine = engine;
|
|
155
|
+
_conn = conn;
|
|
156
|
+
_tx = tx;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public override void Commit() { _tx.Commit(); }
|
|
160
|
+
|
|
161
|
+
public override void Rollback() { _tx.Rollback(); }
|
|
162
|
+
|
|
163
|
+
public override void Dispose()
|
|
164
|
+
{
|
|
165
|
+
_engine.ReleaseTransactionConnection();
|
|
166
|
+
_tx.Dispose();
|
|
167
|
+
_conn.Dispose();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Connection string normalisation ──────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
internal static class SqliteConnectionString
|
|
174
|
+
{
|
|
175
|
+
/// <summary>
|
|
176
|
+
/// Turns the accepted spellings into a Data Source string.
|
|
177
|
+
///
|
|
178
|
+
/// Microsoft.Data.Sqlite only understands `Data Source=`, so the bare
|
|
179
|
+
/// paths and `sqlite:` URLs the rest of the ORM accepts have to be
|
|
180
|
+
/// rewritten. An in-memory database has to stay open for the life of the
|
|
181
|
+
/// connection, which is why callers keep one connection per operation
|
|
182
|
+
/// open only for the duration of a statement.
|
|
183
|
+
/// </summary>
|
|
184
|
+
public static string Normalize(string connectionString)
|
|
185
|
+
{
|
|
186
|
+
var cs = (connectionString ?? "").Trim();
|
|
187
|
+
if (cs.Length == 0) return "Data Source=:memory:";
|
|
188
|
+
|
|
189
|
+
// Already a Microsoft.Data.Sqlite connection string.
|
|
190
|
+
if (cs.IndexOf("Data Source", StringComparison.OrdinalIgnoreCase) >= 0
|
|
191
|
+
|| cs.IndexOf("DataSource", StringComparison.OrdinalIgnoreCase) >= 0
|
|
192
|
+
|| cs.IndexOf("Mode", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
193
|
+
return cs;
|
|
194
|
+
|
|
195
|
+
if (cs == ":memory:")
|
|
196
|
+
return "Data Source=:memory:";
|
|
197
|
+
|
|
198
|
+
if (cs.StartsWith("sqlite://", StringComparison.OrdinalIgnoreCase))
|
|
199
|
+
return "Data Source=" + cs.Substring("sqlite://".Length);
|
|
200
|
+
|
|
201
|
+
if (cs.StartsWith("sqlite:", StringComparison.OrdinalIgnoreCase))
|
|
202
|
+
{
|
|
203
|
+
// `sqlite:app.db` and `sqlite::memory:`
|
|
204
|
+
return "Data Source=" + cs.Substring("sqlite:".Length);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (cs.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
|
|
208
|
+
return cs; // SQLite URI, understood as-is.
|
|
209
|
+
|
|
210
|
+
// A bare path.
|
|
211
|
+
return "Data Source=" + cs;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -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
|
+
}
|