@an5/adapters 0.1.0
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 +263 -0
- package/dotnet/An5Adapter.cs +275 -0
- package/dotnet/Base/Core.cs +82 -0
- package/dotnet/Mssql/MssqlEngine.cs +144 -0
- package/dotnet/Postgres/PostgresEngine.cs +121 -0
- package/package.json +46 -0
- package/python/an5_adapter.py +97 -0
- package/python/base/__init__.py +3 -0
- package/python/base/dialects.py +8 -0
- package/python/base/metadata.py +9 -0
- package/python/base/sql.py +121 -0
- package/python/mssql/__init__.py +1 -0
- package/python/mssql/provider.py +62 -0
- package/python/postgres/__init__.py +1 -0
- package/python/postgres/provider.py +15 -0
- package/python/table_client.py +209 -0
- package/typescript/an5Adapter.d.ts +117 -0
- package/typescript/an5Adapter.js +447 -0
- package/typescript/an5Adapter.ts +474 -0
- package/typescript/base/index.d.ts +3 -0
- package/typescript/base/index.js +19 -0
- package/typescript/base/index.ts +3 -0
- package/typescript/base/metadata.d.ts +14 -0
- package/typescript/base/metadata.js +17 -0
- package/typescript/base/metadata.ts +28 -0
- package/typescript/base/sql.d.ts +4 -0
- package/typescript/base/sql.js +143 -0
- package/typescript/base/sql.ts +98 -0
- package/typescript/base/types.d.ts +21 -0
- package/typescript/base/types.js +2 -0
- package/typescript/base/types.ts +24 -0
- package/typescript/config.d.ts +17 -0
- package/typescript/config.js +92 -0
- package/typescript/config.ts +95 -0
- package/typescript/googlesheets/adapter.d.ts +31 -0
- package/typescript/googlesheets/adapter.js +138 -0
- package/typescript/googlesheets/adapter.ts +167 -0
- package/typescript/googlesheets/an5SheetsAdapter.d.ts +3 -0
- package/typescript/googlesheets/an5SheetsAdapter.js +8 -0
- package/typescript/googlesheets/an5SheetsAdapter.ts +3 -0
- package/typescript/googlesheets/config.d.ts +17 -0
- package/typescript/googlesheets/config.js +29 -0
- package/typescript/googlesheets/config.ts +33 -0
- package/typescript/googlesheets/helpers.d.ts +9 -0
- package/typescript/googlesheets/helpers.js +155 -0
- package/typescript/googlesheets/helpers.ts +124 -0
- package/typescript/googlesheets/index.d.ts +2 -0
- package/typescript/googlesheets/index.js +9 -0
- package/typescript/googlesheets/index.ts +2 -0
- package/typescript/googlesheets/parseConnectionString.d.ts +11 -0
- package/typescript/googlesheets/parseConnectionString.js +44 -0
- package/typescript/googlesheets/parseConnectionString.ts +44 -0
- package/typescript/googlesheets/retry.d.ts +3 -0
- package/typescript/googlesheets/retry.js +26 -0
- package/typescript/googlesheets/retry.ts +23 -0
- package/typescript/googlesheets/sqlExecutor.d.ts +12 -0
- package/typescript/googlesheets/sqlExecutor.js +306 -0
- package/typescript/googlesheets/sqlExecutor.ts +301 -0
- package/typescript/googlesheets/tableClient.d.ts +78 -0
- package/typescript/googlesheets/tableClient.js +394 -0
- package/typescript/googlesheets/tableClient.ts +434 -0
- package/typescript/googlesheets/types.d.ts +4 -0
- package/typescript/googlesheets/types.js +3 -0
- package/typescript/googlesheets/types.ts +6 -0
- package/typescript/index.d.ts +2 -0
- package/typescript/index.js +14 -0
- package/typescript/index.ts +15 -0
- package/typescript/mssql/engine.d.ts +16 -0
- package/typescript/mssql/engine.js +138 -0
- package/typescript/mssql/engine.ts +132 -0
- package/typescript/mssql/index.d.ts +1 -0
- package/typescript/mssql/index.js +17 -0
- package/typescript/mssql/index.ts +1 -0
- package/typescript/mysql/engine.d.ts +13 -0
- package/typescript/mysql/engine.js +77 -0
- package/typescript/mysql/engine.ts +80 -0
- package/typescript/mysql/index.d.ts +1 -0
- package/typescript/mysql/index.js +17 -0
- package/typescript/mysql/index.ts +1 -0
- package/typescript/postgres/engine.d.ts +13 -0
- package/typescript/postgres/engine.js +86 -0
- package/typescript/postgres/engine.ts +87 -0
- package/typescript/postgres/index.d.ts +1 -0
- package/typescript/postgres/index.js +17 -0
- package/typescript/postgres/index.ts +1 -0
- package/typescript/sqlite/engine.d.ts +13 -0
- package/typescript/sqlite/engine.js +58 -0
- package/typescript/sqlite/engine.ts +55 -0
- package/typescript/sqlite/index.d.ts +1 -0
- package/typescript/sqlite/index.js +17 -0
- package/typescript/sqlite/index.ts +1 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Data;
|
|
4
|
+
using System.Reflection;
|
|
5
|
+
|
|
6
|
+
namespace An5Orm
|
|
7
|
+
{
|
|
8
|
+
// ─── MSSQL Engine ──────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
internal class MssqlEngine : IQueryEngine
|
|
11
|
+
{
|
|
12
|
+
public Dialect Dialect => Dialect.Mssql;
|
|
13
|
+
private readonly string _connectionString;
|
|
14
|
+
private readonly int _commandTimeout;
|
|
15
|
+
|
|
16
|
+
[ThreadStatic] private static System.Data.SqlClient.SqlConnection _txConn;
|
|
17
|
+
[ThreadStatic] private static System.Data.SqlClient.SqlTransaction _tx;
|
|
18
|
+
|
|
19
|
+
public MssqlEngine(string connectionString, int commandTimeout)
|
|
20
|
+
{
|
|
21
|
+
_connectionString = connectionString;
|
|
22
|
+
_commandTimeout = commandTimeout;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private System.Data.SqlClient.SqlConnection OpenConnection(out bool isInTransaction)
|
|
26
|
+
{
|
|
27
|
+
if (_txConn != null) { isInTransaction = true; return _txConn; }
|
|
28
|
+
isInTransaction = false;
|
|
29
|
+
var conn = new System.Data.SqlClient.SqlConnection(_connectionString);
|
|
30
|
+
conn.Open();
|
|
31
|
+
return conn;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private System.Data.SqlClient.SqlCommand BuildCommand(
|
|
35
|
+
System.Data.SqlClient.SqlConnection conn, string sql, Dictionary<string, object> parameters)
|
|
36
|
+
{
|
|
37
|
+
var cmd = new System.Data.SqlClient.SqlCommand(sql, conn) { CommandTimeout = _commandTimeout };
|
|
38
|
+
if (_tx != null) cmd.Transaction = _tx;
|
|
39
|
+
if (parameters != null)
|
|
40
|
+
{
|
|
41
|
+
foreach (var kv in parameters)
|
|
42
|
+
{
|
|
43
|
+
var paramName = kv.Key.StartsWith("@") ? kv.Key : "@" + kv.Key;
|
|
44
|
+
cmd.Parameters.AddWithValue(paramName, kv.Value ?? DBNull.Value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return cmd;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public List<Dictionary<string, object>> QueryRaw(string sql, Dictionary<string, object> parameters)
|
|
51
|
+
{
|
|
52
|
+
var results = new List<Dictionary<string, object>>();
|
|
53
|
+
var conn = OpenConnection(out bool isTx);
|
|
54
|
+
try
|
|
55
|
+
{
|
|
56
|
+
using var cmd = BuildCommand(conn, sql, parameters);
|
|
57
|
+
using var reader = cmd.ExecuteReader();
|
|
58
|
+
while (reader.Read())
|
|
59
|
+
{
|
|
60
|
+
var row = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
61
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
62
|
+
{
|
|
63
|
+
row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
|
|
64
|
+
}
|
|
65
|
+
results.Add(row);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public List<T> QueryRaw<T>(string sql, Dictionary<string, object> parameters) where T : new()
|
|
73
|
+
{
|
|
74
|
+
var results = new List<T>();
|
|
75
|
+
var conn = OpenConnection(out bool isTx);
|
|
76
|
+
try
|
|
77
|
+
{
|
|
78
|
+
using var cmd = BuildCommand(conn, sql, parameters);
|
|
79
|
+
using var reader = cmd.ExecuteReader();
|
|
80
|
+
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
81
|
+
while (reader.Read())
|
|
82
|
+
{
|
|
83
|
+
var item = new T();
|
|
84
|
+
foreach (var prop in props)
|
|
85
|
+
{
|
|
86
|
+
if (!HasColumn(reader, prop.Name)) continue;
|
|
87
|
+
var val = reader[prop.Name];
|
|
88
|
+
if (val == DBNull.Value) continue;
|
|
89
|
+
try { prop.SetValue(item, Convert.ChangeType(val, prop.PropertyType)); } catch { }
|
|
90
|
+
}
|
|
91
|
+
results.Add(item);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public int ExecuteRaw(string sql, Dictionary<string, object> parameters)
|
|
99
|
+
{
|
|
100
|
+
var conn = OpenConnection(out bool isTx);
|
|
101
|
+
try
|
|
102
|
+
{
|
|
103
|
+
using var cmd = BuildCommand(conn, sql, parameters);
|
|
104
|
+
return cmd.ExecuteNonQuery();
|
|
105
|
+
}
|
|
106
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private static bool HasColumn(System.Data.SqlClient.SqlDataReader reader, string name)
|
|
110
|
+
{
|
|
111
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
112
|
+
if (reader.GetName(i).Equals(name, StringComparison.OrdinalIgnoreCase)) return true;
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public An5TransactionBase BeginTransaction()
|
|
117
|
+
{
|
|
118
|
+
var conn = new System.Data.SqlClient.SqlConnection(_connectionString);
|
|
119
|
+
conn.Open();
|
|
120
|
+
var tx = conn.BeginTransaction();
|
|
121
|
+
_txConn = conn;
|
|
122
|
+
_tx = tx;
|
|
123
|
+
return new MssqlTransaction(conn, tx, () => { _txConn = null; _tx = null; });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
internal class MssqlTransaction : An5TransactionBase
|
|
128
|
+
{
|
|
129
|
+
private readonly System.Data.SqlClient.SqlConnection _conn;
|
|
130
|
+
private readonly System.Data.SqlClient.SqlTransaction _tx;
|
|
131
|
+
private readonly Action _cleanup;
|
|
132
|
+
|
|
133
|
+
public MssqlTransaction(System.Data.SqlClient.SqlConnection conn, System.Data.SqlClient.SqlTransaction tx, Action cleanup)
|
|
134
|
+
{
|
|
135
|
+
_conn = conn;
|
|
136
|
+
_tx = tx;
|
|
137
|
+
_cleanup = cleanup;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public override void Commit() { _tx.Commit(); _cleanup(); }
|
|
141
|
+
public override void Rollback() { _tx.Rollback(); _cleanup(); }
|
|
142
|
+
public override void Dispose() { _tx.Dispose(); _conn.Dispose(); _cleanup(); }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Data;
|
|
4
|
+
using System.Reflection;
|
|
5
|
+
|
|
6
|
+
namespace An5Orm
|
|
7
|
+
{
|
|
8
|
+
// ─── PostgreSQL Engine ─────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
internal class PostgresEngine : IQueryEngine
|
|
11
|
+
{
|
|
12
|
+
public Dialect Dialect => Dialect.Postgres;
|
|
13
|
+
private readonly string _connectionString;
|
|
14
|
+
private readonly int _commandTimeout;
|
|
15
|
+
|
|
16
|
+
public PostgresEngine(string connectionString, int commandTimeout)
|
|
17
|
+
{
|
|
18
|
+
_connectionString = connectionString;
|
|
19
|
+
_commandTimeout = commandTimeout;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private Npgsql.NpgsqlConnection OpenConnection()
|
|
23
|
+
{
|
|
24
|
+
var conn = new Npgsql.NpgsqlConnection(_connectionString);
|
|
25
|
+
conn.Open();
|
|
26
|
+
return conn;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private Npgsql.NpgsqlCommand BuildCommand(
|
|
30
|
+
Npgsql.NpgsqlConnection conn, string sql, Dictionary<string, object> parameters)
|
|
31
|
+
{
|
|
32
|
+
var cmd = new Npgsql.NpgsqlCommand(sql, conn) { CommandTimeout = _commandTimeout };
|
|
33
|
+
if (parameters != null)
|
|
34
|
+
{
|
|
35
|
+
foreach (var kv in parameters)
|
|
36
|
+
{
|
|
37
|
+
var paramName = kv.Key.StartsWith("@") ? kv.Key : "@" + kv.Key;
|
|
38
|
+
cmd.Parameters.AddWithValue(paramName, kv.Value ?? DBNull.Value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return cmd;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public List<Dictionary<string, object>> QueryRaw(string sql, Dictionary<string, object> parameters)
|
|
45
|
+
{
|
|
46
|
+
var results = new List<Dictionary<string, object>>();
|
|
47
|
+
using var conn = OpenConnection();
|
|
48
|
+
using var cmd = BuildCommand(conn, sql, parameters);
|
|
49
|
+
using var reader = cmd.ExecuteReader();
|
|
50
|
+
while (reader.Read())
|
|
51
|
+
{
|
|
52
|
+
var row = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
53
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
54
|
+
{
|
|
55
|
+
row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
|
|
56
|
+
}
|
|
57
|
+
results.Add(row);
|
|
58
|
+
}
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public List<T> QueryRaw<T>(string sql, Dictionary<string, object> parameters) where T : new()
|
|
63
|
+
{
|
|
64
|
+
var results = new List<T>();
|
|
65
|
+
using var conn = OpenConnection();
|
|
66
|
+
using var cmd = BuildCommand(conn, sql, parameters);
|
|
67
|
+
using var reader = cmd.ExecuteReader();
|
|
68
|
+
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
69
|
+
while (reader.Read())
|
|
70
|
+
{
|
|
71
|
+
var item = new T();
|
|
72
|
+
foreach (var prop in props)
|
|
73
|
+
{
|
|
74
|
+
if (!HasColumn(reader, prop.Name)) continue;
|
|
75
|
+
var val = reader[prop.Name];
|
|
76
|
+
if (val == DBNull.Value) continue;
|
|
77
|
+
try { prop.SetValue(item, Convert.ChangeType(val, prop.PropertyType)); } catch { }
|
|
78
|
+
}
|
|
79
|
+
results.Add(item);
|
|
80
|
+
}
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public int ExecuteRaw(string sql, Dictionary<string, object> parameters)
|
|
85
|
+
{
|
|
86
|
+
using var conn = OpenConnection();
|
|
87
|
+
using var cmd = BuildCommand(conn, sql, parameters);
|
|
88
|
+
return cmd.ExecuteNonQuery();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static bool HasColumn(Npgsql.NpgsqlDataReader reader, string name)
|
|
92
|
+
{
|
|
93
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
94
|
+
if (reader.GetName(i).Equals(name, StringComparison.OrdinalIgnoreCase)) return true;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public An5TransactionBase BeginTransaction()
|
|
99
|
+
{
|
|
100
|
+
var conn = OpenConnection();
|
|
101
|
+
var tx = conn.BeginTransaction();
|
|
102
|
+
return new PostgresTransaction(conn, tx);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
internal class PostgresTransaction : An5TransactionBase
|
|
107
|
+
{
|
|
108
|
+
private readonly Npgsql.NpgsqlConnection _conn;
|
|
109
|
+
private readonly Npgsql.NpgsqlTransaction _tx;
|
|
110
|
+
|
|
111
|
+
public PostgresTransaction(Npgsql.NpgsqlConnection conn, Npgsql.NpgsqlTransaction tx)
|
|
112
|
+
{
|
|
113
|
+
_conn = conn;
|
|
114
|
+
_tx = tx;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public override void Commit() { _tx.Commit(); }
|
|
118
|
+
public override void Rollback() { _tx.Rollback(); }
|
|
119
|
+
public override void Dispose() { _tx.Dispose(); _conn.Dispose(); }
|
|
120
|
+
}
|
|
121
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@an5/adapters",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Standalone language adapters for AN5 ORM - TypeScript, Python, .NET query execution.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "typescript/index.js",
|
|
7
|
+
"types": "typescript/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./typescript/index.js",
|
|
10
|
+
"./typescript": "./typescript/index.js",
|
|
11
|
+
"./typescript/*": "./typescript/*",
|
|
12
|
+
"./python": "./python/an5_adapter.py",
|
|
13
|
+
"./dotnet": "./dotnet/An5Adapter.cs",
|
|
14
|
+
"./config": "./typescript/config.js"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/an5ORM/an5Adapters.git"
|
|
19
|
+
},
|
|
20
|
+
"keywords": ["an5", "orm", "adapter", "typescript", "python", "dotnet"],
|
|
21
|
+
"files": [
|
|
22
|
+
"typescript/**/*.{ts,js,d.ts}",
|
|
23
|
+
"python/**/*.py",
|
|
24
|
+
"dotnet/**/*",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"test": "node test/unit.test.js",
|
|
30
|
+
"test:python": "python -m compileall python 2>&1"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"googleapis": "^134.0.0",
|
|
34
|
+
"mssql": "^12.5.5",
|
|
35
|
+
"pg": "^8.13.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/mssql": "^9.1.8",
|
|
39
|
+
"@types/node": "^20.11.0",
|
|
40
|
+
"@types/pg": "^8.11.0",
|
|
41
|
+
"typescript": "^6.0.3"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Standalone Python runtime adapter for AN5 ORM."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from .base import DIALECT_MSSQL, DIALECT_POSTGRES, detect_dialect, set_adapter_metadata
|
|
7
|
+
from .mssql import connect as connect_mssql
|
|
8
|
+
from .postgres import connect as connect_postgres
|
|
9
|
+
from .table_client import AdapterTableClient
|
|
10
|
+
except ImportError:
|
|
11
|
+
from base import DIALECT_MSSQL, DIALECT_POSTGRES, detect_dialect, set_adapter_metadata
|
|
12
|
+
from mssql import connect as connect_mssql
|
|
13
|
+
from postgres import connect as connect_postgres
|
|
14
|
+
from table_client import AdapterTableClient
|
|
15
|
+
|
|
16
|
+
# Backward-compatible aliases used by tests and older imports.
|
|
17
|
+
_detect_dialect = detect_dialect
|
|
18
|
+
try:
|
|
19
|
+
from .mssql import parse_connection_string as _parse_connection_string
|
|
20
|
+
except ImportError:
|
|
21
|
+
from mssql import parse_connection_string as _parse_connection_string
|
|
22
|
+
|
|
23
|
+
class An5Adapter:
|
|
24
|
+
def __init__(self, connection_string: str):
|
|
25
|
+
self._dialect = detect_dialect(connection_string)
|
|
26
|
+
self._conn_str = connection_string
|
|
27
|
+
|
|
28
|
+
def _connect(self):
|
|
29
|
+
if self._dialect == DIALECT_POSTGRES:
|
|
30
|
+
return connect_postgres(self._conn_str)
|
|
31
|
+
return connect_mssql(self._conn_str)
|
|
32
|
+
|
|
33
|
+
def _to_dicts(self, cursor, query: str) -> List[Dict]:
|
|
34
|
+
if cursor.description:
|
|
35
|
+
cols = [col[0] for col in cursor.description]
|
|
36
|
+
rows = cursor.fetchall() if cursor.description else []
|
|
37
|
+
return [dict(zip(cols, row)) for row in rows]
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
def exec(self, query: str, params: Optional[List] = None) -> List[Dict]:
|
|
41
|
+
conn = self._connect()
|
|
42
|
+
try:
|
|
43
|
+
cursor = conn.cursor()
|
|
44
|
+
cursor.execute(query, params or [])
|
|
45
|
+
return self._to_dicts(cursor, query)
|
|
46
|
+
finally:
|
|
47
|
+
conn.close()
|
|
48
|
+
|
|
49
|
+
def execute(self, query: str, params: Optional[List] = None) -> int:
|
|
50
|
+
conn = self._connect()
|
|
51
|
+
try:
|
|
52
|
+
cursor = conn.cursor()
|
|
53
|
+
cursor.execute(query, params or [])
|
|
54
|
+
return cursor.rowcount
|
|
55
|
+
finally:
|
|
56
|
+
conn.close()
|
|
57
|
+
|
|
58
|
+
def query_raw(self, query: str, *values) -> List[Dict]:
|
|
59
|
+
return self.exec(query, list(values))
|
|
60
|
+
|
|
61
|
+
def execute_raw(self, query: str, *values) -> int:
|
|
62
|
+
return self.execute(query, list(values))
|
|
63
|
+
|
|
64
|
+
def table(self, model_name: str) -> AdapterTableClient:
|
|
65
|
+
return AdapterTableClient(self, model_name)
|
|
66
|
+
|
|
67
|
+
def __getattr__(self, model_name: str) -> AdapterTableClient:
|
|
68
|
+
return self.table(model_name)
|
|
69
|
+
|
|
70
|
+
def transaction(self, fn):
|
|
71
|
+
conn = self._connect()
|
|
72
|
+
conn.autocommit = False
|
|
73
|
+
try:
|
|
74
|
+
result = fn(self)
|
|
75
|
+
conn.commit()
|
|
76
|
+
return result
|
|
77
|
+
except Exception:
|
|
78
|
+
conn.rollback()
|
|
79
|
+
raise
|
|
80
|
+
finally:
|
|
81
|
+
if self._dialect == DIALECT_POSTGRES:
|
|
82
|
+
conn.autocommit = True
|
|
83
|
+
conn.close()
|
|
84
|
+
|
|
85
|
+
def create_an5_adapter(connection_string: str) -> An5Adapter:
|
|
86
|
+
return An5Adapter(connection_string)
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
"An5Adapter",
|
|
90
|
+
"AdapterTableClient",
|
|
91
|
+
"create_an5_adapter",
|
|
92
|
+
"DIALECT_MSSQL",
|
|
93
|
+
"DIALECT_POSTGRES",
|
|
94
|
+
"_detect_dialect",
|
|
95
|
+
"_parse_connection_string",
|
|
96
|
+
"set_adapter_metadata",
|
|
97
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
DIALECT_MSSQL = "mssql"
|
|
2
|
+
DIALECT_POSTGRES = "postgres"
|
|
3
|
+
|
|
4
|
+
def detect_dialect(connection_string: str) -> str:
|
|
5
|
+
cs = connection_string.strip().lower()
|
|
6
|
+
if cs.startswith("postgres://") or cs.startswith("postgresql://"):
|
|
7
|
+
return DIALECT_POSTGRES
|
|
8
|
+
return DIALECT_MSSQL
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
3
|
+
model_to_table: Dict[str, str] = {}
|
|
4
|
+
model_fields: Dict[str, Any] = {}
|
|
5
|
+
|
|
6
|
+
def set_adapter_metadata(metadata: Dict[str, Any]) -> None:
|
|
7
|
+
global model_to_table, model_fields
|
|
8
|
+
model_to_table = dict(metadata.get("model_to_table") or metadata.get("modelToTable") or {})
|
|
9
|
+
model_fields = dict(metadata.get("model_fields") or metadata.get("modelFields") or {})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
from .dialects import DIALECT_MSSQL, DIALECT_POSTGRES
|
|
3
|
+
from .metadata import model_to_table, model_fields
|
|
4
|
+
|
|
5
|
+
# ─── Quoting ───────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
def _quote(name: str, dialect: str) -> str:
|
|
8
|
+
if dialect == DIALECT_POSTGRES:
|
|
9
|
+
if name.startswith("[") and name.endswith("]"):
|
|
10
|
+
return f'"{name[1:-1]}"'
|
|
11
|
+
return f'"{name}"'
|
|
12
|
+
return name if name.startswith("[") else f"[{name}]"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ─── Connection helpers live in provider modules ────────────────────────────────
|
|
16
|
+
|
|
17
|
+
# ─── Where clause builder ──────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
def _parse_where(model_name: str, where: Optional[Dict], params: Dict, dialect: str, prefix: str = "") -> str:
|
|
20
|
+
if not where:
|
|
21
|
+
return ""
|
|
22
|
+
conditions: List[str] = []
|
|
23
|
+
|
|
24
|
+
for key, value in where.items():
|
|
25
|
+
if key == "OR" and isinstance(value, list):
|
|
26
|
+
sub = [_parse_where(model_name, v, params, dialect, f"{prefix}or{i}_") for i, v in enumerate(value)]
|
|
27
|
+
sub = [s for s in sub if s]
|
|
28
|
+
if sub:
|
|
29
|
+
conditions.append(f"({' OR '.join(sub)})")
|
|
30
|
+
elif key == "AND" and isinstance(value, list):
|
|
31
|
+
sub = [_parse_where(model_name, v, params, dialect, f"{prefix}and{i}_") for i, v in enumerate(value)]
|
|
32
|
+
sub = [s for s in sub if s]
|
|
33
|
+
if sub:
|
|
34
|
+
conditions.append(f"({' AND '.join(sub)})")
|
|
35
|
+
else:
|
|
36
|
+
col = _quote(key, dialect)
|
|
37
|
+
pname = f"{prefix}{key}"
|
|
38
|
+
placeholder = "%s" if dialect == DIALECT_POSTGRES else "?"
|
|
39
|
+
if value is None:
|
|
40
|
+
conditions.append(f"{col} IS NULL")
|
|
41
|
+
elif isinstance(value, dict):
|
|
42
|
+
if "not" in value:
|
|
43
|
+
if value["not"] is None:
|
|
44
|
+
conditions.append(f"{col} IS NOT NULL")
|
|
45
|
+
else:
|
|
46
|
+
params[f"{pname}_not"] = value["not"]
|
|
47
|
+
conditions.append(f"{col} <> {placeholder}")
|
|
48
|
+
if "equals" in value:
|
|
49
|
+
params[f"{pname}_eq"] = value["equals"]
|
|
50
|
+
conditions.append(f"{col} = {placeholder}")
|
|
51
|
+
if "contains" in value:
|
|
52
|
+
params[f"{pname}_co"] = f"%{value['contains']}%"
|
|
53
|
+
conditions.append(f"{col} LIKE {placeholder}")
|
|
54
|
+
if "startsWith" in value:
|
|
55
|
+
params[f"{pname}_sw"] = f"{value['startsWith']}%"
|
|
56
|
+
conditions.append(f"{col} LIKE {placeholder}")
|
|
57
|
+
if "endsWith" in value:
|
|
58
|
+
params[f"{pname}_ew"] = f"%{value['endsWith']}"
|
|
59
|
+
conditions.append(f"{col} LIKE {placeholder}")
|
|
60
|
+
if "gte" in value:
|
|
61
|
+
params[f"{pname}_gte"] = value["gte"]
|
|
62
|
+
conditions.append(f"{col} >= {placeholder}")
|
|
63
|
+
if "lte" in value:
|
|
64
|
+
params[f"{pname}_lte"] = value["lte"]
|
|
65
|
+
conditions.append(f"{col} <= {placeholder}")
|
|
66
|
+
if "gt" in value:
|
|
67
|
+
params[f"{pname}_gt"] = value["gt"]
|
|
68
|
+
conditions.append(f"{col} > {placeholder}")
|
|
69
|
+
if "lt" in value:
|
|
70
|
+
params[f"{pname}_lt"] = value["lt"]
|
|
71
|
+
conditions.append(f"{col} < {placeholder}")
|
|
72
|
+
if "in" in value:
|
|
73
|
+
vals = value["in"]
|
|
74
|
+
if vals:
|
|
75
|
+
placeholders = ", ".join([placeholder] * len(vals))
|
|
76
|
+
for i, v in enumerate(vals):
|
|
77
|
+
params[f"{pname}_in{i}"] = v
|
|
78
|
+
conditions.append(f"{col} IN ({placeholders})")
|
|
79
|
+
else:
|
|
80
|
+
conditions.append("1=0")
|
|
81
|
+
else:
|
|
82
|
+
params[pname] = value
|
|
83
|
+
conditions.append(f"{col} = {placeholder}")
|
|
84
|
+
|
|
85
|
+
return " AND ".join(conditions)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _build_order_by(order_by: Any, dialect: str) -> str:
|
|
89
|
+
if not order_by:
|
|
90
|
+
return ""
|
|
91
|
+
entries = order_by if isinstance(order_by, list) else [order_by]
|
|
92
|
+
parts: List[str] = []
|
|
93
|
+
for entry in entries:
|
|
94
|
+
for col, direction in entry.items():
|
|
95
|
+
parts.append(f"{_quote(col, dialect)} {str(direction).upper()}")
|
|
96
|
+
return f"ORDER BY {', '.join(parts)}" if parts else ""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ─── Table name resolution ─────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
def _resolve_table(model_name: str) -> str:
|
|
102
|
+
if model_name in model_to_table:
|
|
103
|
+
return model_to_table[model_name]
|
|
104
|
+
camel = model_name[0].lower() + model_name[1:] if model_name else model_name
|
|
105
|
+
if camel in model_to_table:
|
|
106
|
+
return model_to_table[camel]
|
|
107
|
+
lower = model_name.lower()
|
|
108
|
+
if lower in model_to_table:
|
|
109
|
+
return model_to_table[lower]
|
|
110
|
+
return model_name
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _quote_table(t: str, dialect: str) -> str:
|
|
114
|
+
q = lambda s: f'"{s}"' if dialect == DIALECT_POSTGRES else f"[{s}]"
|
|
115
|
+
if t.startswith("[") or t.startswith('"'):
|
|
116
|
+
return t
|
|
117
|
+
if "." in t:
|
|
118
|
+
return ".".join(q(p) for p in t.split("."))
|
|
119
|
+
return q(t)
|
|
120
|
+
|
|
121
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .provider import PYODBC_AVAILABLE, connect, parse_connection_string, placeholder
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import pyodbc
|
|
3
|
+
PYODBC_AVAILABLE = True
|
|
4
|
+
except ImportError:
|
|
5
|
+
pyodbc = None
|
|
6
|
+
PYODBC_AVAILABLE = False
|
|
7
|
+
|
|
8
|
+
def parse_connection_string(url: str) -> str:
|
|
9
|
+
"""Convert sqlserver:// URL to pyodbc connection string."""
|
|
10
|
+
url = url.replace("sqlserver://", "")
|
|
11
|
+
parts = url.split(";")
|
|
12
|
+
host_part = parts[0]
|
|
13
|
+
host, _, port = host_part.partition(":")
|
|
14
|
+
port = port or "1433"
|
|
15
|
+
|
|
16
|
+
config: Dict[str, str] = {"server": host, "port": port}
|
|
17
|
+
for part in parts[1:]:
|
|
18
|
+
if "=" not in part:
|
|
19
|
+
continue
|
|
20
|
+
k, _, v = part.partition("=")
|
|
21
|
+
k = k.strip().lower()
|
|
22
|
+
v = v.strip()
|
|
23
|
+
if k in ("database",):
|
|
24
|
+
config["database"] = v
|
|
25
|
+
elif k in ("user", "uid"):
|
|
26
|
+
config["user"] = v
|
|
27
|
+
elif k in ("password", "pwd"):
|
|
28
|
+
config["password"] = v
|
|
29
|
+
elif k == "encrypt":
|
|
30
|
+
config["encrypt"] = "yes" if v.lower() == "true" else "no"
|
|
31
|
+
elif k == "trustservercertificate":
|
|
32
|
+
config["trust"] = "yes" if v.lower() == "true" else "no"
|
|
33
|
+
|
|
34
|
+
encrypt = config.get("encrypt", "yes")
|
|
35
|
+
trust = config.get("trust", "yes")
|
|
36
|
+
if "user" in config:
|
|
37
|
+
return (
|
|
38
|
+
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
|
39
|
+
f"SERVER={config['server']},{config['port']};"
|
|
40
|
+
f"DATABASE={config.get('database', '')};"
|
|
41
|
+
f"UID={config['user']};"
|
|
42
|
+
f"PWD={config.get('password', '')};"
|
|
43
|
+
f"Encrypt={encrypt};TrustServerCertificate={trust};"
|
|
44
|
+
)
|
|
45
|
+
# Windows auth
|
|
46
|
+
return (
|
|
47
|
+
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
|
48
|
+
f"SERVER={config['server']},{config['port']};"
|
|
49
|
+
f"DATABASE={config.get('database', '')};"
|
|
50
|
+
f"Trusted_Connection=yes;"
|
|
51
|
+
f"Encrypt={encrypt};TrustServerCertificate={trust};"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def connect(connection_string: str):
|
|
57
|
+
if not PYODBC_AVAILABLE:
|
|
58
|
+
raise ImportError("pyodbc is required: pip install pyodbc")
|
|
59
|
+
return pyodbc.connect(parse_connection_string(connection_string), autocommit=True)
|
|
60
|
+
|
|
61
|
+
def placeholder() -> str:
|
|
62
|
+
return "?"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .provider import PSYCOPG2_AVAILABLE, connect, placeholder
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import psycopg2
|
|
3
|
+
import psycopg2.extras
|
|
4
|
+
PSYCOPG2_AVAILABLE = True
|
|
5
|
+
except ImportError:
|
|
6
|
+
psycopg2 = None
|
|
7
|
+
PSYCOPG2_AVAILABLE = False
|
|
8
|
+
|
|
9
|
+
def connect(connection_string: str):
|
|
10
|
+
if not PSYCOPG2_AVAILABLE:
|
|
11
|
+
raise ImportError("psycopg2 is required: pip install psycopg2-binary")
|
|
12
|
+
return psycopg2.connect(connection_string)
|
|
13
|
+
|
|
14
|
+
def placeholder() -> str:
|
|
15
|
+
return "%s"
|