@an5/adapters 0.2.6 → 0.2.10
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 +96 -2
- package/dist/an5Adapter.d.ts +57 -0
- package/dist/an5Adapter.js +263 -4
- package/dist/base/sql.js +16 -15
- package/dist/base/types.d.ts +6 -0
- package/dist/googlesheets/helpers.js +13 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -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/sqlite/engine.js +2 -1
- package/dotnet/An5Adapter.cs +41 -18
- package/dotnet/Base/Core.cs +56 -26
- package/dotnet/Postgres/PostgresEngine.cs +58 -30
- package/dotnet/Sqlite/SqliteEngine.cs +214 -0
- package/golang/base/where.go +32 -49
- package/golang/base/where_test.go +57 -0
- package/package.json +26 -11
- package/python/an5_adapter.py +74 -9
- package/python/base/__init__.py +1 -1
- package/python/base/metadata.py +25 -1
- package/python/base/sql.py +50 -21
- package/python/sqlite/__init__.py +3 -0
- package/python/sqlite/provider.py +83 -0
- package/python/table_client.py +53 -36
- package/rust/Cargo.toml +31 -0
- package/rust/README.md +71 -0
- package/rust/src/adapter.rs +1314 -0
- package/rust/src/base/dialect.rs +201 -0
- package/rust/src/base/metadata.rs +138 -0
- package/rust/src/base/mod.rs +17 -0
- package/rust/src/base/where_builder.rs +386 -0
- package/rust/src/lib.rs +75 -0
- package/typescript/src/an5Adapter.ts +300 -4
- package/typescript/src/base/sql.ts +15 -14
- package/typescript/src/base/types.ts +6 -0
- package/typescript/src/googlesheets/helpers.ts +11 -8
- package/typescript/src/index.ts +17 -0
- package/typescript/src/nbase/client.ts +214 -0
- package/typescript/src/nbase/index.ts +104 -0
- package/typescript/src/sqlite/engine.ts +2 -1
package/dotnet/Base/Core.cs
CHANGED
|
@@ -7,7 +7,8 @@ namespace An5Orm
|
|
|
7
7
|
public enum Dialect
|
|
8
8
|
{
|
|
9
9
|
Mssql,
|
|
10
|
-
Postgres
|
|
10
|
+
Postgres,
|
|
11
|
+
Sqlite
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
// ─── Config ────────────────────────────────────────────────────────────────
|
|
@@ -39,6 +40,38 @@ public enum Dialect
|
|
|
39
40
|
public abstract void Dispose();
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
// ─── Connection scope ──────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/// <summary>
|
|
46
|
+
/// A connection borrowed for the length of one operation, and whether the
|
|
47
|
+
/// caller still owns it.
|
|
48
|
+
///
|
|
49
|
+
/// A statement issued while a transaction is open has to run on that
|
|
50
|
+
/// transaction's connection. Opening a second one is not merely wasteful: on
|
|
51
|
+
/// SQLite the open transaction holds a write lock, so the second connection
|
|
52
|
+
/// is refused with "database is locked", and on the server engines the write
|
|
53
|
+
/// silently lands outside the transaction, so commit and rollback do not
|
|
54
|
+
/// cover it. Each engine keeps the transaction connection and hands it back
|
|
55
|
+
/// through this scope while it is set.
|
|
56
|
+
/// </summary>
|
|
57
|
+
internal readonly struct ConnectionScope<T> : IDisposable where T : class
|
|
58
|
+
{
|
|
59
|
+
public T Connection { get; }
|
|
60
|
+
|
|
61
|
+
private readonly bool _owned;
|
|
62
|
+
|
|
63
|
+
internal ConnectionScope(T connection, bool owned)
|
|
64
|
+
{
|
|
65
|
+
Connection = connection;
|
|
66
|
+
_owned = owned;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public void Dispose()
|
|
70
|
+
{
|
|
71
|
+
if (_owned && Connection is IDisposable disposable) disposable.Dispose();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
42
75
|
// ─── Dialect detection ─────────────────────────────────────────────────────
|
|
43
76
|
|
|
44
77
|
internal static class DialectDetector
|
|
@@ -46,6 +79,13 @@ public enum Dialect
|
|
|
46
79
|
public static Dialect Detect(string connectionString)
|
|
47
80
|
{
|
|
48
81
|
var cs = (connectionString ?? "").Trim().ToLowerInvariant();
|
|
82
|
+
// SQLite first: `Data Source=app.db` and a bare file path would
|
|
83
|
+
// otherwise fall through, and a Postgres string never looks like
|
|
84
|
+
// any of these.
|
|
85
|
+
if (cs.StartsWith("sqlite:") || cs.StartsWith("file:") || cs == ":memory:"
|
|
86
|
+
|| cs.Contains("data source=") || cs.Contains("datasource=")
|
|
87
|
+
|| cs.EndsWith(".db") || cs.EndsWith(".sqlite") || cs.EndsWith(".sqlite3"))
|
|
88
|
+
return Dialect.Sqlite;
|
|
49
89
|
if (cs.StartsWith("postgres://") || cs.StartsWith("postgresql://") || cs.Contains("host="))
|
|
50
90
|
return Dialect.Postgres;
|
|
51
91
|
return Dialect.Mssql;
|
|
@@ -66,7 +106,7 @@ public enum Dialect
|
|
|
66
106
|
public static string QuoteName(string name, Dialect dialect)
|
|
67
107
|
{
|
|
68
108
|
var raw = name ?? "";
|
|
69
|
-
if (dialect == Dialect.Postgres)
|
|
109
|
+
if (dialect == Dialect.Postgres || dialect == Dialect.Sqlite)
|
|
70
110
|
{
|
|
71
111
|
var unwrapped = StripWrapping(StripWrapping(raw, "[", "]"), "\"", "\"");
|
|
72
112
|
return "\"" + unwrapped.Replace("\"", "\"\"") + "\"";
|
|
@@ -219,40 +259,30 @@ public enum Dialect
|
|
|
219
259
|
{
|
|
220
260
|
var key = kv.Key;
|
|
221
261
|
var value = kv.Value;
|
|
222
|
-
if (key == "OR"
|
|
262
|
+
if (key == "OR" || key == "AND" || key == "NOT")
|
|
223
263
|
{
|
|
264
|
+
var items = value is System.Collections.IList list
|
|
265
|
+
? list : new object[] { value };
|
|
224
266
|
var subs = new List<string>();
|
|
225
267
|
var i = 0;
|
|
226
|
-
foreach (var item in
|
|
268
|
+
foreach (var item in items)
|
|
227
269
|
{
|
|
228
270
|
if (item is Dictionary<string, object> subMap)
|
|
229
271
|
{
|
|
230
|
-
var s = ParseWhere(subMap, parameters, dialect, $"{prefix}
|
|
231
|
-
|
|
272
|
+
var s = ParseWhere(subMap, parameters, dialect, $"{prefix}{key}{i++}_");
|
|
273
|
+
subs.Add(string.IsNullOrEmpty(s) ? "1=1" : s);
|
|
232
274
|
}
|
|
233
275
|
}
|
|
234
|
-
if (subs.Count
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
if (key == "AND" && value is System.Collections.IList andList && andList.Count > 0)
|
|
238
|
-
{
|
|
239
|
-
var subs = new List<string>();
|
|
240
|
-
var i = 0;
|
|
241
|
-
foreach (var item in andList)
|
|
276
|
+
if (subs.Count == 0)
|
|
242
277
|
{
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
278
|
+
if (key == "OR") conditions.Add("1=0");
|
|
279
|
+
}
|
|
280
|
+
else
|
|
281
|
+
{
|
|
282
|
+
var joiner = key == "AND" ? " AND " : " OR ";
|
|
283
|
+
var negation = key == "NOT" ? "NOT " : "";
|
|
284
|
+
conditions.Add(negation + "(" + string.Join(joiner, subs) + ")");
|
|
248
285
|
}
|
|
249
|
-
if (subs.Count > 0) conditions.Add("(" + string.Join(" AND ", subs) + ")");
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
if (key == "NOT" && value is Dictionary<string, object> notMap)
|
|
253
|
-
{
|
|
254
|
-
var s = ParseWhere(notMap, parameters, dialect, $"{prefix}not_");
|
|
255
|
-
if (!string.IsNullOrEmpty(s)) conditions.Add("NOT (" + s + ")");
|
|
256
286
|
continue;
|
|
257
287
|
}
|
|
258
288
|
|
|
@@ -13,14 +13,22 @@ namespace An5Orm
|
|
|
13
13
|
private readonly string _connectionString;
|
|
14
14
|
private readonly int _commandTimeout;
|
|
15
15
|
|
|
16
|
+
// Same shape as MssqlEngine: while a transaction is open, statements
|
|
17
|
+
// have to run on its connection and carry its transaction, or they land
|
|
18
|
+
// outside it and commit/rollback do not cover them.
|
|
19
|
+
[ThreadStatic] private static Npgsql.NpgsqlConnection _txConn;
|
|
20
|
+
[ThreadStatic] private static Npgsql.NpgsqlTransaction _tx;
|
|
21
|
+
|
|
16
22
|
public PostgresEngine(string connectionString, int commandTimeout)
|
|
17
23
|
{
|
|
18
24
|
_connectionString = connectionString;
|
|
19
25
|
_commandTimeout = commandTimeout;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
private Npgsql.NpgsqlConnection OpenConnection()
|
|
28
|
+
private Npgsql.NpgsqlConnection OpenConnection(out bool isInTransaction)
|
|
23
29
|
{
|
|
30
|
+
if (_txConn != null) { isInTransaction = true; return _txConn; }
|
|
31
|
+
isInTransaction = false;
|
|
24
32
|
var conn = new Npgsql.NpgsqlConnection(_connectionString);
|
|
25
33
|
conn.Open();
|
|
26
34
|
return conn;
|
|
@@ -30,6 +38,7 @@ namespace An5Orm
|
|
|
30
38
|
Npgsql.NpgsqlConnection conn, string sql, Dictionary<string, object> parameters)
|
|
31
39
|
{
|
|
32
40
|
var cmd = new Npgsql.NpgsqlCommand(sql, conn) { CommandTimeout = _commandTimeout };
|
|
41
|
+
if (_tx != null) cmd.Transaction = _tx;
|
|
33
42
|
if (parameters != null)
|
|
34
43
|
{
|
|
35
44
|
foreach (var kv in parameters)
|
|
@@ -44,48 +53,60 @@ namespace An5Orm
|
|
|
44
53
|
public List<Dictionary<string, object>> QueryRaw(string sql, Dictionary<string, object> parameters)
|
|
45
54
|
{
|
|
46
55
|
var results = new List<Dictionary<string, object>>();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
using var reader = cmd.ExecuteReader();
|
|
50
|
-
while (reader.Read())
|
|
56
|
+
var conn = OpenConnection(out bool isTx);
|
|
57
|
+
try
|
|
51
58
|
{
|
|
52
|
-
var
|
|
53
|
-
|
|
59
|
+
using var cmd = BuildCommand(conn, sql, parameters);
|
|
60
|
+
using var reader = cmd.ExecuteReader();
|
|
61
|
+
while (reader.Read())
|
|
54
62
|
{
|
|
55
|
-
row
|
|
63
|
+
var row = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
64
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
65
|
+
{
|
|
66
|
+
row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
|
|
67
|
+
}
|
|
68
|
+
results.Add(row);
|
|
56
69
|
}
|
|
57
|
-
results.Add(row);
|
|
58
70
|
}
|
|
71
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
59
72
|
return results;
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
public List<T> QueryRaw<T>(string sql, Dictionary<string, object> parameters) where T : new()
|
|
63
76
|
{
|
|
64
77
|
var results = new List<T>();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
using var reader = cmd.ExecuteReader();
|
|
68
|
-
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
69
|
-
while (reader.Read())
|
|
78
|
+
var conn = OpenConnection(out bool isTx);
|
|
79
|
+
try
|
|
70
80
|
{
|
|
71
|
-
var
|
|
72
|
-
|
|
81
|
+
using var cmd = BuildCommand(conn, sql, parameters);
|
|
82
|
+
using var reader = cmd.ExecuteReader();
|
|
83
|
+
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
84
|
+
while (reader.Read())
|
|
73
85
|
{
|
|
74
|
-
|
|
75
|
-
var
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
var item = new T();
|
|
87
|
+
foreach (var prop in props)
|
|
88
|
+
{
|
|
89
|
+
if (!HasColumn(reader, prop.Name)) continue;
|
|
90
|
+
var val = reader[prop.Name];
|
|
91
|
+
if (val == DBNull.Value) continue;
|
|
92
|
+
try { prop.SetValue(item, Convert.ChangeType(val, prop.PropertyType)); } catch { }
|
|
93
|
+
}
|
|
94
|
+
results.Add(item);
|
|
78
95
|
}
|
|
79
|
-
results.Add(item);
|
|
80
96
|
}
|
|
97
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
81
98
|
return results;
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
public int ExecuteRaw(string sql, Dictionary<string, object> parameters)
|
|
85
102
|
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
var conn = OpenConnection(out bool isTx);
|
|
104
|
+
try
|
|
105
|
+
{
|
|
106
|
+
using var cmd = BuildCommand(conn, sql, parameters);
|
|
107
|
+
return cmd.ExecuteNonQuery();
|
|
108
|
+
}
|
|
109
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
89
110
|
}
|
|
90
111
|
|
|
91
112
|
private static bool HasColumn(Npgsql.NpgsqlDataReader reader, string name)
|
|
@@ -97,9 +118,14 @@ namespace An5Orm
|
|
|
97
118
|
|
|
98
119
|
public An5TransactionBase BeginTransaction()
|
|
99
120
|
{
|
|
100
|
-
|
|
121
|
+
if (_txConn != null)
|
|
122
|
+
throw new InvalidOperationException("A transaction is already open on this thread.");
|
|
123
|
+
var conn = new Npgsql.NpgsqlConnection(_connectionString);
|
|
124
|
+
conn.Open();
|
|
101
125
|
var tx = conn.BeginTransaction();
|
|
102
|
-
|
|
126
|
+
_txConn = conn;
|
|
127
|
+
_tx = tx;
|
|
128
|
+
return new PostgresTransaction(conn, tx, () => { _txConn = null; _tx = null; });
|
|
103
129
|
}
|
|
104
130
|
}
|
|
105
131
|
|
|
@@ -107,15 +133,17 @@ namespace An5Orm
|
|
|
107
133
|
{
|
|
108
134
|
private readonly Npgsql.NpgsqlConnection _conn;
|
|
109
135
|
private readonly Npgsql.NpgsqlTransaction _tx;
|
|
136
|
+
private readonly Action _cleanup;
|
|
110
137
|
|
|
111
|
-
public PostgresTransaction(Npgsql.NpgsqlConnection conn, Npgsql.NpgsqlTransaction tx)
|
|
138
|
+
public PostgresTransaction(Npgsql.NpgsqlConnection conn, Npgsql.NpgsqlTransaction tx, Action cleanup)
|
|
112
139
|
{
|
|
113
140
|
_conn = conn;
|
|
114
141
|
_tx = tx;
|
|
142
|
+
_cleanup = cleanup;
|
|
115
143
|
}
|
|
116
144
|
|
|
117
|
-
public override void Commit() { _tx.Commit(); }
|
|
118
|
-
public override void Rollback() { _tx.Rollback(); }
|
|
119
|
-
public override void Dispose() { _tx.Dispose(); _conn.Dispose(); }
|
|
145
|
+
public override void Commit() { _tx.Commit(); _cleanup(); }
|
|
146
|
+
public override void Rollback() { _tx.Rollback(); _cleanup(); }
|
|
147
|
+
public override void Dispose() { _tx.Dispose(); _conn.Dispose(); _cleanup(); }
|
|
120
148
|
}
|
|
121
149
|
}
|
|
@@ -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
|
+
}
|
package/golang/base/where.go
CHANGED
|
@@ -80,55 +80,41 @@ func buildWhereRec(where map[string]interface{}, dialect Dialect, args *[]interf
|
|
|
80
80
|
value := where[key]
|
|
81
81
|
|
|
82
82
|
switch key {
|
|
83
|
-
case "OR":
|
|
84
|
-
if list, ok := toSlice(value); ok && len(list) > 0 {
|
|
85
|
-
var subs []string
|
|
86
|
-
for i, item := range list {
|
|
87
|
-
if m, ok := item.(map[string]interface{}); ok && m != nil {
|
|
88
|
-
if s := buildWhereRec(m, dialect, args, ph, fmt.Sprintf("%sor%d_", prefix, i)); s != "" {
|
|
89
|
-
subs = append(subs, s)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if len(subs) > 0 {
|
|
94
|
-
conditions = append(conditions, "("+strings.Join(subs, " OR ")+")")
|
|
95
|
-
}
|
|
96
|
-
continue
|
|
97
|
-
}
|
|
98
|
-
case "AND":
|
|
99
|
-
if list, ok := toSlice(value); ok && len(list) > 0 {
|
|
100
|
-
var subs []string
|
|
101
|
-
for i, item := range list {
|
|
102
|
-
if m, ok := item.(map[string]interface{}); ok && m != nil {
|
|
103
|
-
if s := buildWhereRec(m, dialect, args, ph, fmt.Sprintf("%sand%d_", prefix, i)); s != "" {
|
|
104
|
-
subs = append(subs, s)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if len(subs) > 0 {
|
|
109
|
-
conditions = append(conditions, "("+strings.Join(subs, " AND ")+")")
|
|
110
|
-
}
|
|
111
|
-
continue
|
|
112
|
-
}
|
|
113
|
-
case "NOT":
|
|
83
|
+
case "OR", "AND", "NOT":
|
|
114
84
|
items := value
|
|
115
85
|
if m, ok := value.(map[string]interface{}); ok {
|
|
116
86
|
items = []interface{}{m}
|
|
117
87
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
88
|
+
list, ok := toSlice(items)
|
|
89
|
+
if !ok {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
var subs []string
|
|
93
|
+
for i, item := range list {
|
|
94
|
+
if m, ok := item.(map[string]interface{}); ok {
|
|
95
|
+
s := buildWhereRec(cleanWhere(m), dialect, args, ph, fmt.Sprintf("%s%s%d_", prefix, key, i))
|
|
96
|
+
if s == "" {
|
|
97
|
+
s = "1=1"
|
|
125
98
|
}
|
|
99
|
+
subs = append(subs, s)
|
|
126
100
|
}
|
|
127
|
-
|
|
128
|
-
|
|
101
|
+
}
|
|
102
|
+
if len(subs) == 0 {
|
|
103
|
+
if key == "OR" {
|
|
104
|
+
conditions = append(conditions, "1=0")
|
|
129
105
|
}
|
|
130
|
-
|
|
106
|
+
} else {
|
|
107
|
+
joiner := " OR "
|
|
108
|
+
if key == "AND" {
|
|
109
|
+
joiner = " AND "
|
|
110
|
+
}
|
|
111
|
+
clause := "(" + strings.Join(subs, joiner) + ")"
|
|
112
|
+
if key == "NOT" {
|
|
113
|
+
clause = "NOT " + clause
|
|
114
|
+
}
|
|
115
|
+
conditions = append(conditions, clause)
|
|
131
116
|
}
|
|
117
|
+
continue
|
|
132
118
|
}
|
|
133
119
|
|
|
134
120
|
conditions = append(conditions, buildFieldCondition(key, value, dialect, args, ph, prefix)...)
|
|
@@ -171,13 +157,10 @@ func buildFieldCondition(key string, value interface{}, dialect Dialect, args *[
|
|
|
171
157
|
case "not":
|
|
172
158
|
if val == nil {
|
|
173
159
|
parts = append(parts, col+" IS NOT NULL")
|
|
174
|
-
} else if nested, ok := val.(map[string]interface{}); ok
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if nestedCond != "" {
|
|
179
|
-
parts = append(parts, "NOT ("+nestedCond+")")
|
|
180
|
-
}
|
|
160
|
+
} else if nested, ok := val.(map[string]interface{}); ok {
|
|
161
|
+
nestedCond := buildWhereRec(map[string]interface{}{key: nested}, dialect, args, ph, key+"_not_")
|
|
162
|
+
if nestedCond != "" {
|
|
163
|
+
parts = append(parts, "NOT ("+nestedCond+")")
|
|
181
164
|
}
|
|
182
165
|
} else {
|
|
183
166
|
parts = append(parts, fmt.Sprintf("%s <> %s", col, addArg(val)))
|
|
@@ -308,4 +291,4 @@ func BuildOrderBy(orderBy interface{}, dialect Dialect) string {
|
|
|
308
291
|
return ""
|
|
309
292
|
}
|
|
310
293
|
return "ORDER BY " + strings.Join(parts, ", ")
|
|
311
|
-
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package base
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"os/exec"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"reflect"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// SQLite is provided by Python's standard library to keep the adapter driver-neutral.
|
|
13
|
+
func TestSharedQueryContract(t *testing.T) {
|
|
14
|
+
fixtures, err := os.ReadFile(filepath.Join("..", "..", "test", "fixtures", "query-semantics.json"))
|
|
15
|
+
if err != nil {
|
|
16
|
+
t.Fatal(err)
|
|
17
|
+
}
|
|
18
|
+
var contract struct {
|
|
19
|
+
Rows []map[string]interface{} `json:"rows"`
|
|
20
|
+
Cases []struct {
|
|
21
|
+
Name string `json:"name"`
|
|
22
|
+
Where map[string]interface{} `json:"where"`
|
|
23
|
+
IDs []int `json:"ids"`
|
|
24
|
+
} `json:"cases"`
|
|
25
|
+
}
|
|
26
|
+
if err := json.Unmarshal(fixtures, &contract); err != nil {
|
|
27
|
+
t.Fatal(err)
|
|
28
|
+
}
|
|
29
|
+
python, err := exec.LookPath("python3")
|
|
30
|
+
if err != nil {
|
|
31
|
+
t.Fatal("python3 is required for the SQLite query contract")
|
|
32
|
+
}
|
|
33
|
+
for _, c := range contract.Cases {
|
|
34
|
+
t.Run(c.Name, func(t *testing.T) {
|
|
35
|
+
clause, args := BuildWhere(c.Where, DialectSqlite, func(int) string { return "?" })
|
|
36
|
+
payload, _ := json.Marshal(map[string]interface{}{"rows": contract.Rows, "clause": clause, "args": args})
|
|
37
|
+
cmd := exec.Command(python, "-c", `import json,sqlite3,sys
|
|
38
|
+
p=json.loads(sys.argv[1])
|
|
39
|
+
db=sqlite3.connect(':memory:')
|
|
40
|
+
db.execute('CREATE TABLE users (id INTEGER, score INTEGER)')
|
|
41
|
+
db.executemany('INSERT INTO users VALUES (?,?)',[(r['id'],r['score']) for r in p['rows']])
|
|
42
|
+
sql='SELECT id FROM users'+(' WHERE '+p['clause'] if p['clause'] else '')+' ORDER BY id'
|
|
43
|
+
print(json.dumps([r[0] for r in db.execute(sql,p['args'])]))`, string(payload))
|
|
44
|
+
output, err := cmd.CombinedOutput()
|
|
45
|
+
if err != nil {
|
|
46
|
+
t.Fatalf("query failed: %v\n%s", err, output)
|
|
47
|
+
}
|
|
48
|
+
var got []int
|
|
49
|
+
if err := json.Unmarshal(output, &got); err != nil {
|
|
50
|
+
t.Fatal(err)
|
|
51
|
+
}
|
|
52
|
+
if !reflect.DeepEqual(got, c.IDs) {
|
|
53
|
+
t.Fatalf("got %v, want %v", got, c.IDs)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|