@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.
Files changed (43) hide show
  1. package/README.md +96 -2
  2. package/dist/an5Adapter.d.ts +57 -0
  3. package/dist/an5Adapter.js +263 -4
  4. package/dist/base/sql.js +16 -15
  5. package/dist/base/types.d.ts +6 -0
  6. package/dist/googlesheets/helpers.js +13 -8
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +9 -1
  9. package/dist/nbase/client.d.ts +92 -0
  10. package/dist/nbase/client.js +147 -0
  11. package/dist/nbase/index.d.ts +47 -0
  12. package/dist/nbase/index.js +80 -0
  13. package/dist/sqlite/engine.js +2 -1
  14. package/dotnet/An5Adapter.cs +41 -18
  15. package/dotnet/Base/Core.cs +56 -26
  16. package/dotnet/Postgres/PostgresEngine.cs +58 -30
  17. package/dotnet/Sqlite/SqliteEngine.cs +214 -0
  18. package/golang/base/where.go +32 -49
  19. package/golang/base/where_test.go +57 -0
  20. package/package.json +26 -11
  21. package/python/an5_adapter.py +74 -9
  22. package/python/base/__init__.py +1 -1
  23. package/python/base/metadata.py +25 -1
  24. package/python/base/sql.py +50 -21
  25. package/python/sqlite/__init__.py +3 -0
  26. package/python/sqlite/provider.py +83 -0
  27. package/python/table_client.py +53 -36
  28. package/rust/Cargo.toml +31 -0
  29. package/rust/README.md +71 -0
  30. package/rust/src/adapter.rs +1314 -0
  31. package/rust/src/base/dialect.rs +201 -0
  32. package/rust/src/base/metadata.rs +138 -0
  33. package/rust/src/base/mod.rs +17 -0
  34. package/rust/src/base/where_builder.rs +386 -0
  35. package/rust/src/lib.rs +75 -0
  36. package/typescript/src/an5Adapter.ts +300 -4
  37. package/typescript/src/base/sql.ts +15 -14
  38. package/typescript/src/base/types.ts +6 -0
  39. package/typescript/src/googlesheets/helpers.ts +11 -8
  40. package/typescript/src/index.ts +17 -0
  41. package/typescript/src/nbase/client.ts +214 -0
  42. package/typescript/src/nbase/index.ts +104 -0
  43. package/typescript/src/sqlite/engine.ts +2 -1
@@ -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" && value is System.Collections.IList orList && orList.Count > 0)
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 orList)
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}or{i++}_");
231
- if (!string.IsNullOrEmpty(s)) subs.Add(s);
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 > 0) conditions.Add("(" + string.Join(" OR ", subs) + ")");
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 (item is Dictionary<string, object> subMap)
244
- {
245
- var s = ParseWhere(subMap, parameters, dialect, $"{prefix}and{i++}_");
246
- if (!string.IsNullOrEmpty(s)) subs.Add(s);
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
- using var conn = OpenConnection();
48
- using var cmd = BuildCommand(conn, sql, parameters);
49
- using var reader = cmd.ExecuteReader();
50
- while (reader.Read())
56
+ var conn = OpenConnection(out bool isTx);
57
+ try
51
58
  {
52
- var row = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
53
- for (int i = 0; i < reader.FieldCount; i++)
59
+ using var cmd = BuildCommand(conn, sql, parameters);
60
+ using var reader = cmd.ExecuteReader();
61
+ while (reader.Read())
54
62
  {
55
- row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
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
- 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())
78
+ var conn = OpenConnection(out bool isTx);
79
+ try
70
80
  {
71
- var item = new T();
72
- foreach (var prop in props)
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
- 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 { }
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
- using var conn = OpenConnection();
87
- using var cmd = BuildCommand(conn, sql, parameters);
88
- return cmd.ExecuteNonQuery();
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
- var conn = OpenConnection();
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
- return new PostgresTransaction(conn, tx);
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
+ }
@@ -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
- if list, ok := toSlice(items); ok && len(list) > 0 {
119
- var subs []string
120
- for i, item := range list {
121
- if m, ok := item.(map[string]interface{}); ok && m != nil {
122
- if s := buildWhereRec(m, dialect, args, ph, fmt.Sprintf("%snot%d_", prefix, i)); s != "" {
123
- subs = append(subs, s)
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
- if len(subs) > 0 {
128
- conditions = append(conditions, "NOT ("+strings.Join(subs, " AND ")+")")
101
+ }
102
+ if len(subs) == 0 {
103
+ if key == "OR" {
104
+ conditions = append(conditions, "1=0")
129
105
  }
130
- continue
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 && !isOperatorMap(val) {
175
- if nestedArgs := len(*args); true {
176
- nestedCond := buildWhereRec(map[string]interface{}{key: nested}, dialect, args, ph, key+"_not_")
177
- _ = nestedArgs
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
+ }