@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
@@ -0,0 +1,386 @@
1
+ //! WHERE and ORDER BY builders for the AN5 Rust adapter.
2
+ //!
3
+ //! Accepts the same ORM-style `serde_json::Value` payloads as the Go and
4
+ //! TypeScript adapters, so `{"name": {"contains": "Jo"}}` behaves the same
5
+ //! everywhere.
6
+
7
+ use serde_json::{Map, Value};
8
+
9
+ use super::dialect::Dialect;
10
+
11
+ /// Comparison operators understood inside a filter object.
12
+ const OPERATOR_KEYS: [&str; 12] = [
13
+ "equals", "in", "notIn", "contains", "startsWith", "endsWith", "not", "gte", "lte", "gt",
14
+ "lt", "mode",
15
+ ];
16
+
17
+ /// A bound SQL argument.
18
+ pub type SqlArg = serde_json::Value;
19
+
20
+ /// Accumulated SQL fragment plus its ordered bind arguments.
21
+ #[derive(Debug, Default, Clone, PartialEq)]
22
+ pub struct Where {
23
+ pub clause: String,
24
+ pub args: Vec<SqlArg>,
25
+ }
26
+
27
+ impl Where {
28
+ pub fn is_empty(&self) -> bool {
29
+ self.clause.is_empty()
30
+ }
31
+ }
32
+
33
+ fn is_operator_map(v: &Value) -> bool {
34
+ match v.as_object() {
35
+ Some(m) => OPERATOR_KEYS.iter().any(|k| m.contains_key(*k)),
36
+ None => false,
37
+ }
38
+ }
39
+
40
+ fn as_object(v: &Value) -> Option<&Map<String, Value>> {
41
+ match v {
42
+ Value::Object(m) => Some(m),
43
+ _ => None,
44
+ }
45
+ }
46
+
47
+ fn to_list(v: &Value) -> Vec<Value> {
48
+ match v {
49
+ Value::Array(items) => items.clone(),
50
+ other => vec![other.clone()],
51
+ }
52
+ }
53
+
54
+ fn to_string_value(v: &Value) -> String {
55
+ match v {
56
+ Value::String(s) => s.clone(),
57
+ other => other.to_string(),
58
+ }
59
+ }
60
+
61
+ /// Flattens relation-style nested where objects (`user` -> `{email: ...}`).
62
+ fn clean_where(where_obj: &Map<String, Value>) -> Map<String, Value> {
63
+ let mut clean = Map::new();
64
+ for (k, v) in where_obj {
65
+ if k.contains('_') {
66
+ match as_object(v) {
67
+ Some(inner) if !is_operator_map(v) => {
68
+ for (nk, nv) in inner {
69
+ clean.insert(nk.clone(), nv.clone());
70
+ }
71
+ }
72
+ _ => {
73
+ clean.insert(k.clone(), v.clone());
74
+ }
75
+ }
76
+ } else {
77
+ clean.insert(k.clone(), v.clone());
78
+ }
79
+ }
80
+ clean
81
+ }
82
+
83
+ /// Build a WHERE clause with 1-based placeholders from `ph`.
84
+ pub fn build_where(where_obj: &Value, dialect: Dialect, ph: &dyn Fn(usize) -> String) -> Where {
85
+ let mut out = Where::default();
86
+ if let Some(obj) = as_object(where_obj) {
87
+ let cleaned = clean_where(obj);
88
+ out.clause = build_where_rec(&cleaned, dialect, &mut out.args, ph);
89
+ }
90
+ out
91
+ }
92
+
93
+ fn build_where_rec(
94
+ where_obj: &Map<String, Value>,
95
+ dialect: Dialect,
96
+ args: &mut Vec<SqlArg>,
97
+ ph: &dyn Fn(usize) -> String,
98
+ ) -> String {
99
+ if where_obj.is_empty() {
100
+ return String::new();
101
+ }
102
+ let mut conditions: Vec<String> = Vec::new();
103
+
104
+ // Deterministic key order so the generated SQL is stable.
105
+ let mut keys: Vec<&String> = where_obj.keys().collect();
106
+ keys.sort();
107
+
108
+ for key in keys {
109
+ let value = &where_obj[key];
110
+
111
+ match key.as_str() {
112
+ "AND" | "OR" | "NOT" => {
113
+ let mut subs: Vec<String> = Vec::new();
114
+ for item in to_list(value) {
115
+ if let Some(m) = as_object(&item) {
116
+ let s = build_where_rec(&clean_where(m), dialect, args, ph);
117
+ subs.push(if s.is_empty() { "1=1".to_string() } else { s });
118
+ }
119
+ }
120
+ if subs.is_empty() {
121
+ if key == "OR" { conditions.push("1=0".to_string()); }
122
+ } else {
123
+ let joiner = if key == "AND" { " AND " } else { " OR " };
124
+ let negation = if key == "NOT" { "NOT " } else { "" };
125
+ conditions.push(format!("{}({})", negation, subs.join(joiner)));
126
+ }
127
+ continue;
128
+ }
129
+ _ => {}
130
+ }
131
+
132
+ conditions.extend(build_field_condition(key, value, dialect, args, ph));
133
+ }
134
+
135
+ conditions.join(" AND ")
136
+ }
137
+
138
+ fn build_field_condition(
139
+ key: &str,
140
+ value: &Value,
141
+ dialect: Dialect,
142
+ args: &mut Vec<SqlArg>,
143
+ ph: &dyn Fn(usize) -> String,
144
+ ) -> Vec<String> {
145
+ let col = dialect.quote_identifier(key);
146
+
147
+ if value.is_null() {
148
+ return vec![format!("{} IS NULL", col)];
149
+ }
150
+
151
+ let map = match as_object(value) {
152
+ Some(m) => m,
153
+ None => {
154
+ let p = push_arg(args, value.clone(), ph);
155
+ return vec![format!("{} = {}", col, p)];
156
+ }
157
+ };
158
+
159
+ let mut parts: Vec<String> = Vec::new();
160
+ let mut op_keys: Vec<&String> = map.keys().collect();
161
+ op_keys.sort();
162
+
163
+ for op in op_keys {
164
+ let val = &map[op];
165
+ let p = |args: &mut Vec<SqlArg>| push_arg(args, val.clone(), ph);
166
+ match op.as_str() {
167
+ "not" => {
168
+ if val.is_null() {
169
+ parts.push(format!("{} IS NOT NULL", col));
170
+ } else if is_operator_map(val) {
171
+ let mut nested = Map::new();
172
+ nested.insert(key.to_string(), val.clone());
173
+ let cond = build_where_rec(&nested, dialect, args, ph);
174
+ if !cond.is_empty() {
175
+ parts.push(format!("NOT ({})", cond));
176
+ }
177
+ } else {
178
+ let phs = p(args);
179
+ parts.push(format!("{} <> {}", col, phs));
180
+ }
181
+ }
182
+ "equals" => {
183
+ if val.is_null() {
184
+ parts.push(format!("{} IS NULL", col));
185
+ } else {
186
+ let phs = p(args);
187
+ parts.push(format!("{} = {}", col, phs));
188
+ }
189
+ }
190
+ "contains" => {
191
+ let phs = p(args);
192
+ parts.push(format!("{} LIKE {}", col, phs));
193
+ wrap_last_arg(args, |a| format!("%{}%", to_string_value(a)));
194
+ }
195
+ "startsWith" => {
196
+ let phs = p(args);
197
+ parts.push(format!("{} LIKE {}", col, phs));
198
+ wrap_last_arg(args, |a| format!("{}%", to_string_value(a)));
199
+ }
200
+ "endsWith" => {
201
+ let phs = p(args);
202
+ parts.push(format!("{} LIKE {}", col, phs));
203
+ wrap_last_arg(args, |a| format!("%{}", to_string_value(a)));
204
+ }
205
+ "gte" => {
206
+ let phs = p(args);
207
+ parts.push(format!("{} >= {}", col, phs));
208
+ }
209
+ "lte" => {
210
+ let phs = p(args);
211
+ parts.push(format!("{} <= {}", col, phs));
212
+ }
213
+ "gt" => {
214
+ let phs = p(args);
215
+ parts.push(format!("{} > {}", col, phs));
216
+ }
217
+ "lt" => {
218
+ let phs = p(args);
219
+ parts.push(format!("{} < {}", col, phs));
220
+ }
221
+ "in" => {
222
+ let items = to_list(val);
223
+ if items.is_empty() {
224
+ parts.push("1=0".to_string());
225
+ } else {
226
+ let phs: Vec<String> = items
227
+ .iter()
228
+ .map(|it| push_arg(args, it.clone(), ph))
229
+ .collect();
230
+ parts.push(format!("{} IN ({})", col, phs.join(", ")));
231
+ }
232
+ }
233
+ "notIn" => {
234
+ let items = to_list(val);
235
+ if items.is_empty() {
236
+ parts.push("1=1".to_string());
237
+ } else {
238
+ let phs: Vec<String> = items
239
+ .iter()
240
+ .map(|it| push_arg(args, it.clone(), ph))
241
+ .collect();
242
+ parts.push(format!("{} NOT IN ({})", col, phs.join(", ")));
243
+ }
244
+ }
245
+ // `mode: "insensitive"` is accepted and ignored: collation is a
246
+ // database concern, matching the Go adapter's tolerance.
247
+ _ => {}
248
+ }
249
+ }
250
+
251
+ parts
252
+ }
253
+
254
+ fn push_arg(args: &mut Vec<SqlArg>, value: SqlArg, ph: &dyn Fn(usize) -> String) -> String {
255
+ let n = args.len() + 1;
256
+ args.push(value);
257
+ ph(n)
258
+ }
259
+
260
+ /// Rewrites the most recently pushed arg, used by the LIKE operators which need
261
+ /// to wrap the value in wildcards after the placeholder has been allocated.
262
+ fn wrap_last_arg(args: &mut Vec<SqlArg>, f: impl Fn(&SqlArg) -> String) {
263
+ if let Some(last) = args.last_mut() {
264
+ let wrapped = f(last);
265
+ *last = Value::String(wrapped);
266
+ }
267
+ }
268
+
269
+ fn normalize_sort_direction(dir: &Value) -> &'static str {
270
+ match dir.as_str() {
271
+ Some(s) if s.eq_ignore_ascii_case("desc") => "DESC",
272
+ _ => "ASC",
273
+ }
274
+ }
275
+
276
+ /// Build an ORDER BY clause from a map, list of maps, or raw string.
277
+ pub fn build_order_by(order_by: &Value, dialect: Dialect) -> String {
278
+ let mut entries: Vec<&Map<String, Value>> = Vec::new();
279
+
280
+ match order_by {
281
+ Value::String(s) => {
282
+ if s.trim().is_empty() {
283
+ return String::new();
284
+ }
285
+ return format!("ORDER BY {}", s);
286
+ }
287
+ Value::Array(items) => {
288
+ for item in items {
289
+ if let Some(m) = as_object(item) {
290
+ entries.push(m);
291
+ }
292
+ }
293
+ }
294
+ Value::Object(m) => entries.push(m),
295
+ _ => return String::new(),
296
+ }
297
+
298
+ let mut parts: Vec<String> = Vec::new();
299
+ for entry in entries {
300
+ let mut keys: Vec<&String> = entry.keys().collect();
301
+ keys.sort();
302
+ for key in keys {
303
+ parts.push(format!(
304
+ "{} {}",
305
+ dialect.quote_identifier(key),
306
+ normalize_sort_direction(&entry[key])
307
+ ));
308
+ }
309
+ }
310
+
311
+ if parts.is_empty() {
312
+ return String::new();
313
+ }
314
+ format!("ORDER BY {}", parts.join(", "))
315
+ }
316
+
317
+ #[cfg(test)]
318
+ mod tests {
319
+ use super::*;
320
+ use serde_json::json;
321
+
322
+ fn q(n: usize) -> String {
323
+ format!("?{}", n)
324
+ }
325
+
326
+ #[test]
327
+ fn equality_produces_placeholder() {
328
+ let w = build_where(&json!({ "id": "abc" }), Dialect::Mssql, &q);
329
+ assert_eq!(w.clause, "[id] = ?1");
330
+ assert_eq!(w.args, vec![json!("abc")]);
331
+ }
332
+
333
+ #[test]
334
+ fn null_uses_is_null() {
335
+ let w = build_where(&json!({ "name": null }), Dialect::Mssql, &q);
336
+ assert_eq!(w.clause, "[name] IS NULL");
337
+ assert!(w.args.is_empty());
338
+ }
339
+
340
+ #[test]
341
+ fn contains_wraps_wildcards() {
342
+ let w = build_where(
343
+ &json!({ "email": { "contains": "@example.com" } }),
344
+ Dialect::Mssql,
345
+ &q,
346
+ );
347
+ assert_eq!(w.clause, "[email] LIKE ?1");
348
+ assert_eq!(w.args, vec![json!("%@example.com%")]);
349
+ }
350
+
351
+ #[test]
352
+ fn in_expands_each_item() {
353
+ let w = build_where(
354
+ &json!({ "status": { "in": ["a", "b"] } }),
355
+ Dialect::Mssql,
356
+ &q,
357
+ );
358
+ assert_eq!(w.clause, "[status] IN (?1, ?2)");
359
+ assert_eq!(w.args, vec![json!("a"), json!("b")]);
360
+ }
361
+
362
+ #[test]
363
+ fn postgres_placeholders_are_numbered() {
364
+ let ph = |n: usize| format!("${}", n);
365
+ let w = build_where(&json!({ "id": "x" }), Dialect::Postgres, &ph);
366
+ assert_eq!(w.clause, "\"id\" = $1");
367
+ }
368
+
369
+ #[test]
370
+ fn or_blocks_are_grouped() {
371
+ let w = build_where(
372
+ &json!({ "OR": [{ "a": 1 }, { "b": 2 }] }),
373
+ Dialect::Mssql,
374
+ &q,
375
+ );
376
+ assert_eq!(w.clause, "([a] = ?1 OR [b] = ?2)");
377
+ }
378
+
379
+ #[test]
380
+ fn order_by_defaults_to_asc() {
381
+ let sql = build_order_by(&json!({ "name": "desc" }), Dialect::Mssql);
382
+ assert_eq!(sql, "ORDER BY [name] DESC");
383
+ let sql = build_order_by(&json!({ "name": "anything" }), Dialect::Mssql);
384
+ assert_eq!(sql, "ORDER BY [name] ASC");
385
+ }
386
+ }
@@ -0,0 +1,75 @@
1
+ //! AN5 adapters runtime for Rust.
2
+ //!
3
+ //! Provides [`An5Adapter`] (connection + dialect) and [`TableClient`] (ORM CRUD)
4
+ //! on top of `sqlx`, plus a `base` module with dialect-aware SQL building blocks.
5
+ //! Mirrors the Go / TypeScript / Python adapter packages.
6
+ //!
7
+ //! ```no_run
8
+ //! use an5_adapters::{An5Adapter, FindManyArgs};
9
+ //! use serde_json::json;
10
+ //!
11
+ //! # async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
12
+ //! // Register the drivers this binary needs (swap in `sqlx::postgres::any::DRIVER`
13
+ //! // or `sqlx::mysql::any::DRIVER` and enable the matching cargo feature), then
14
+ //! // hand the connection string to the adapter.
15
+ //! sqlx::any::install_drivers(&[sqlx::sqlite::any::DRIVER])?;
16
+ //! let db = An5Adapter::connect("sqlite:file:app.sqlite?mode=rwc").await?;
17
+ //!
18
+ //! // Dynamic access, the Rust equivalent of `db.user` in TypeScript.
19
+ //! let users = db
20
+ //! .table("User")
21
+ //! .find_many(&FindManyArgs {
22
+ //! r#where: Some(json!({ "email": { "contains": "@example.com" } })),
23
+ //! take: 10,
24
+ //! ..Default::default()
25
+ //! })
26
+ //! .await?;
27
+ //! # Ok(()) }
28
+ //! ```
29
+
30
+ pub mod adapter;
31
+ pub mod base;
32
+
33
+ pub use adapter::{
34
+ AggregateArgs, An5Adapter, CountArgs, CreateArgs, CreateManyArgs, DeleteManyArgs, FindManyArgs,
35
+ GroupByArgs, Result, RowMap, TableClient, UpdateArgs, UpdateManyArgs, UpsertArgs,
36
+ VectorSearchArgs, ViewClient,
37
+ };
38
+ pub use base::{
39
+ add_table_override, build_order_by, build_where, cosine_similarity, dot_product,
40
+ euclidean_distance, get_fields_for_model, get_model_to_table, get_relations_for_model,
41
+ resolve_table, set_adapter_metadata, vector_distance, AdapterMetadata, Dialect, RelationDef,
42
+ Where,
43
+ };
44
+
45
+ /// Default connection string: `DATABASE_URL` or a local MSSQL fallback.
46
+ pub fn get_default_connection_string() -> String {
47
+ std::env::var("DATABASE_URL").unwrap_or_else(|_| {
48
+ "Server=localhost;Database=master;Trusted_Connection=True;TrustServerCertificate=True;"
49
+ .to_string()
50
+ })
51
+ }
52
+
53
+ /// Build an adapter using [`get_default_connection_string`].
54
+ pub async fn create_an5_adapter(connection_string: Option<&str>) -> Result<An5Adapter> {
55
+ An5Adapter::connect(connection_string.unwrap_or(&get_default_connection_string())).await
56
+ }
57
+
58
+ #[cfg(test)]
59
+ mod tests {
60
+ use super::*;
61
+
62
+ #[test]
63
+ fn default_connection_string_falls_back_to_mssql() {
64
+ // DATABASE_URL may be set in CI; only assert the shape when it is not.
65
+ if std::env::var("DATABASE_URL").is_err() {
66
+ assert!(get_default_connection_string().starts_with("Server=localhost"));
67
+ }
68
+ }
69
+
70
+ #[test]
71
+ fn exports_are_reachable() {
72
+ assert_eq!(Dialect::detect("sqlite::memory:"), Dialect::Sqlite);
73
+ assert!(!resolve_table("Whatever").is_empty());
74
+ }
75
+ }