@an5/adapters 0.2.5 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +283 -305
  2. package/dist/an5Adapter.d.ts +142 -7
  3. package/dist/an5Adapter.js +998 -68
  4. package/dist/base/metadata.d.ts +10 -0
  5. package/dist/base/metadata.js +107 -0
  6. package/dist/base/sql.d.ts +19 -1
  7. package/dist/base/sql.js +152 -24
  8. package/dist/base/types.d.ts +10 -1
  9. package/dist/browser.d.ts +4 -1
  10. package/dist/browser.js +9 -2
  11. package/dist/googlesheets/adapter.d.ts +1 -0
  12. package/dist/googlesheets/adapter.js +9 -2
  13. package/dist/googlesheets/config.d.ts +4 -4
  14. package/dist/googlesheets/helpers.js +2 -0
  15. package/dist/googlesheets/parseConnectionString.js +5 -2
  16. package/dist/googlesheets/sqlExecutor.js +29 -24
  17. package/dist/googlesheets/tableClient.js +14 -5
  18. package/dist/index.d.ts +5 -1
  19. package/dist/index.js +15 -1
  20. package/dist/mssql/engine.js +8 -4
  21. package/dist/mysql/engine.js +1 -1
  22. package/dist/nbase/client.d.ts +92 -0
  23. package/dist/nbase/client.js +147 -0
  24. package/dist/nbase/index.d.ts +47 -0
  25. package/dist/nbase/index.js +80 -0
  26. package/dist/postgres/engine.js +1 -1
  27. package/dist/sqlite/browserEngine.d.ts +27 -0
  28. package/dist/sqlite/browserEngine.js +161 -0
  29. package/dist/sqlite/engine.js +15 -3
  30. package/dist/sqlite/index.d.ts +1 -0
  31. package/dist/sqlite/index.js +1 -0
  32. package/dist/typed.d.ts +5 -0
  33. package/dist/typed.js +2 -0
  34. package/dotnet/An5Adapter.cs +1112 -275
  35. package/dotnet/Base/Core.cs +380 -82
  36. package/dotnet/Mssql/MssqlEngine.cs +145 -144
  37. package/dotnet/Postgres/PostgresEngine.cs +149 -121
  38. package/dotnet/Sqlite/SqliteEngine.cs +214 -0
  39. package/golang/an5_adapter.go +314 -0
  40. package/golang/base/base.go +100 -0
  41. package/golang/base/metadata.go +116 -0
  42. package/golang/base/where.go +311 -0
  43. package/golang/go.mod +3 -0
  44. package/golang/mssql/mssql.go +32 -0
  45. package/golang/postgres/postgres.go +37 -0
  46. package/golang/sqlite/sqlite.go +14 -0
  47. package/golang/sqlite_test.go +42 -0
  48. package/golang/table_client.go +1329 -0
  49. package/package.json +26 -9
  50. package/python/an5_adapter.py +184 -97
  51. package/python/base/__init__.py +3 -3
  52. package/python/base/dialects.py +11 -8
  53. package/python/base/metadata.py +59 -10
  54. package/python/base/sql.py +202 -121
  55. package/python/mssql/__init__.py +1 -1
  56. package/python/mssql/provider.py +62 -62
  57. package/python/postgres/__init__.py +1 -1
  58. package/python/postgres/provider.py +15 -15
  59. package/python/sqlite/__init__.py +3 -0
  60. package/python/sqlite/provider.py +83 -0
  61. package/python/table_client.py +636 -216
  62. package/rust/Cargo.toml +31 -0
  63. package/rust/README.md +71 -0
  64. package/rust/src/adapter.rs +1312 -0
  65. package/rust/src/base/dialect.rs +172 -0
  66. package/rust/src/base/metadata.rs +134 -0
  67. package/rust/src/base/mod.rs +17 -0
  68. package/rust/src/base/where_builder.rs +405 -0
  69. package/rust/src/lib.rs +75 -0
  70. package/typescript/src/an5Adapter.ts +1515 -505
  71. package/typescript/src/base/index.ts +3 -3
  72. package/typescript/src/base/metadata.ts +148 -28
  73. package/typescript/src/base/sql.ts +264 -98
  74. package/typescript/src/base/types.ts +33 -24
  75. package/typescript/src/browser.ts +10 -6
  76. package/typescript/src/config.ts +95 -95
  77. package/typescript/src/googlesheets/adapter.ts +269 -259
  78. package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
  79. package/typescript/src/googlesheets/config.ts +47 -47
  80. package/typescript/src/googlesheets/helpers.ts +144 -143
  81. package/typescript/src/googlesheets/index.ts +3 -3
  82. package/typescript/src/googlesheets/parseConnectionString.ts +48 -46
  83. package/typescript/src/googlesheets/retry.ts +126 -126
  84. package/typescript/src/googlesheets/sqlExecutor.ts +304 -301
  85. package/typescript/src/googlesheets/tableClient.ts +461 -455
  86. package/typescript/src/googlesheets/types.ts +6 -6
  87. package/typescript/src/index.ts +48 -27
  88. package/typescript/src/mssql/engine.ts +134 -132
  89. package/typescript/src/mssql/index.ts +1 -1
  90. package/typescript/src/mysql/engine.ts +80 -80
  91. package/typescript/src/mysql/index.ts +1 -1
  92. package/typescript/src/nbase/client.ts +214 -0
  93. package/typescript/src/nbase/index.ts +104 -0
  94. package/typescript/src/postgres/engine.ts +87 -87
  95. package/typescript/src/postgres/index.ts +1 -1
  96. package/typescript/src/sqlite/browserEngine.ts +192 -0
  97. package/typescript/src/sqlite/engine.ts +66 -55
  98. package/typescript/src/sqlite/index.ts +3 -1
  99. package/typescript/src/typed.ts +52 -0
@@ -0,0 +1,172 @@
1
+ //! Dialect detection, identifier quoting and vector math.
2
+ //!
3
+ //! Mirrors the Go `an5adapters/base` package.
4
+
5
+ /// Supported database engines.
6
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
7
+ pub enum Dialect {
8
+ Mssql,
9
+ Postgres,
10
+ Sqlite,
11
+ }
12
+
13
+ impl Dialect {
14
+ /// Detect the dialect from a connection string.
15
+ pub fn detect(conn_str: &str) -> Self {
16
+ let l = conn_str.trim().to_lowercase();
17
+ if l.starts_with("postgres://")
18
+ || l.starts_with("postgresql://")
19
+ || l.contains("port=5432")
20
+ {
21
+ return Dialect::Postgres;
22
+ }
23
+ if l.starts_with("sqlite://")
24
+ || l.starts_with("sqlite:")
25
+ || l.starts_with("file:")
26
+ || l.ends_with(".db")
27
+ || l.ends_with(".sqlite")
28
+ || l.ends_with(".sqlite3")
29
+ || l == ":memory:"
30
+ {
31
+ return Dialect::Sqlite;
32
+ }
33
+ Dialect::Mssql
34
+ }
35
+
36
+ /// Quote a single identifier (column or table part).
37
+ pub fn quote_identifier(&self, name: &str) -> String {
38
+ let raw = strip_wrapping(strip_wrapping(name, "[", "]"), "\"", "\"");
39
+ match self {
40
+ Dialect::Postgres | Dialect::Sqlite => {
41
+ format!("\"{}\"", raw.replace('"', "\"\""))
42
+ }
43
+ Dialect::Mssql => format!("[{}]", raw.replace(']', "]]")),
44
+ }
45
+ }
46
+
47
+ /// Quote a possibly schema-qualified table name (`dbo.users`).
48
+ pub fn quote_table(&self, table: &str) -> String {
49
+ let t = table.trim();
50
+ // Already quoted by the generator: `[dbo].[User]`.
51
+ if t.starts_with('[') || t.starts_with('"') {
52
+ return t.to_string();
53
+ }
54
+ t.split('.')
55
+ .map(|p| self.quote_identifier(p))
56
+ .collect::<Vec<_>>()
57
+ .join(".")
58
+ }
59
+
60
+ /// Positional placeholder: `$N` for Postgres, `?` otherwise.
61
+ pub fn placeholder(&self, n: usize) -> String {
62
+ match self {
63
+ Dialect::Postgres => format!("${}", n),
64
+ _ => "?".to_string(),
65
+ }
66
+ }
67
+
68
+ /// MSSQL read hint used on every SELECT.
69
+ pub fn nolock(&self) -> &'static str {
70
+ match self {
71
+ Dialect::Mssql => " WITH (NOLOCK)",
72
+ _ => "",
73
+ }
74
+ }
75
+
76
+ /// Dialects that use `LIMIT` / `OFFSET` instead of `TOP` / `FETCH NEXT`.
77
+ pub fn supports_limit_offset(&self) -> bool {
78
+ matches!(self, Dialect::Postgres | Dialect::Sqlite)
79
+ }
80
+ }
81
+
82
+ fn strip_wrapping<'a>(name: &'a str, left: &str, right: &str) -> &'a str {
83
+ if name.starts_with(left) && name.ends_with(right) && name.len() >= left.len() + right.len() {
84
+ &name[left.len()..name.len() - right.len()]
85
+ } else {
86
+ name
87
+ }
88
+ }
89
+
90
+ /// Cosine similarity between two vectors (1.0 = identical direction).
91
+ pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
92
+ if a.len() != b.len() || a.is_empty() {
93
+ return 0.0;
94
+ }
95
+ let mut dot = 0.0;
96
+ let mut m1 = 0.0;
97
+ let mut m2 = 0.0;
98
+ for i in 0..a.len() {
99
+ dot += a[i] * b[i];
100
+ m1 += a[i] * a[i];
101
+ m2 += b[i] * b[i];
102
+ }
103
+ if m1 == 0.0 || m2 == 0.0 {
104
+ return 0.0;
105
+ }
106
+ dot / (m1.sqrt() * m2.sqrt())
107
+ }
108
+
109
+ /// Euclidean distance between two vectors.
110
+ pub fn euclidean_distance(a: &[f64], b: &[f64]) -> f64 {
111
+ a.iter()
112
+ .zip(b.iter())
113
+ .map(|(x, y)| (x - y) * (x - y))
114
+ .sum::<f64>()
115
+ .sqrt()
116
+ }
117
+
118
+ /// Dot product of two vectors.
119
+ pub fn dot_product(a: &[f64], b: &[f64]) -> f64 {
120
+ a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
121
+ }
122
+
123
+ /// Distance honouring the AN5 metric names (`cosine` / `euclidean` / `dot`).
124
+ pub fn vector_distance(a: &[f64], b: &[f64], metric: &str) -> f64 {
125
+ match metric.to_lowercase().as_str() {
126
+ "euclidean" => euclidean_distance(a, b),
127
+ "dot" => -dot_product(a, b),
128
+ _ => 1.0 - cosine_similarity(a, b),
129
+ }
130
+ }
131
+
132
+ #[cfg(test)]
133
+ mod tests {
134
+ use super::*;
135
+
136
+ #[test]
137
+ fn detects_dialects() {
138
+ assert_eq!(
139
+ Dialect::detect("postgres://localhost/db"),
140
+ Dialect::Postgres
141
+ );
142
+ assert_eq!(Dialect::detect("app.db"), Dialect::Sqlite);
143
+ assert_eq!(
144
+ Dialect::detect("Server=localhost;Database=master;"),
145
+ Dialect::Mssql
146
+ );
147
+ }
148
+
149
+ #[test]
150
+ fn quotes_identifiers_per_dialect() {
151
+ assert_eq!(Dialect::Mssql.quote_identifier("id"), "[id]");
152
+ assert_eq!(Dialect::Postgres.quote_identifier("id"), "\"id\"");
153
+ }
154
+
155
+ #[test]
156
+ fn keeps_generator_quoted_table() {
157
+ assert_eq!(Dialect::Mssql.quote_table("[dbo].[User]"), "[dbo].[User]");
158
+ assert_eq!(Dialect::Postgres.quote_table("dbo.users"), "\"dbo\".\"users\"");
159
+ }
160
+
161
+ #[test]
162
+ fn placeholders_per_dialect() {
163
+ assert_eq!(Dialect::Postgres.placeholder(2), "$2");
164
+ assert_eq!(Dialect::Mssql.placeholder(2), "?");
165
+ }
166
+
167
+ #[test]
168
+ fn cosine_identical_is_one() {
169
+ let v = vec![1.0, 0.0];
170
+ assert!((cosine_similarity(&v, &v) - 1.0).abs() < 1e-9);
171
+ }
172
+ }
@@ -0,0 +1,134 @@
1
+ //! Adapter metadata injected by the generated `an5Client` crate.
2
+ //!
3
+ //! Mirrors the Go `an5adapters/base` metadata store.
4
+
5
+ use std::collections::HashMap;
6
+ use std::sync::{OnceLock, RwLock};
7
+
8
+ /// Describes a model relation, used for eager-loading (`include`) support.
9
+ #[derive(Debug, Clone, Default, PartialEq, Eq)]
10
+ pub struct RelationDef {
11
+ pub model_name: String,
12
+ /// `many` or `one`.
13
+ pub relation_type: String,
14
+ pub foreign_key: String,
15
+ pub local_key: String,
16
+ }
17
+
18
+ /// Metadata bundle provided by the generated client.
19
+ #[derive(Debug, Clone, Default)]
20
+ pub struct AdapterMetadata {
21
+ pub model_to_table: HashMap<String, String>,
22
+ pub model_fields: HashMap<String, serde_json::Value>,
23
+ pub relation_map: HashMap<String, HashMap<String, RelationDef>>,
24
+ }
25
+
26
+ static MODEL_TO_TABLE: OnceLock<RwLock<HashMap<String, String>>> = OnceLock::new();
27
+ static MODEL_FIELDS: OnceLock<RwLock<HashMap<String, serde_json::Value>>> = OnceLock::new();
28
+ static RELATION_MAP: OnceLock<RwLock<HashMap<String, HashMap<String, RelationDef>>>> = OnceLock::new();
29
+ static TABLE_OVERRIDES: OnceLock<RwLock<HashMap<String, String>>> = OnceLock::new();
30
+
31
+ fn table_overrides_store() -> &'static RwLock<HashMap<String, String>> {
32
+ TABLE_OVERRIDES.get_or_init(|| RwLock::new(HashMap::new()))
33
+ }
34
+
35
+ /// Point a model alias at a different physical table.
36
+ pub fn add_table_override(model: &str, table: &str) {
37
+ if let Ok(mut store) = table_overrides_store().write() {
38
+ store.insert(model.to_string(), table.to_string());
39
+ }
40
+ }
41
+
42
+ fn model_to_table_store() -> &'static RwLock<HashMap<String, String>> {
43
+ MODEL_TO_TABLE.get_or_init(|| RwLock::new(HashMap::new()))
44
+ }
45
+
46
+ fn model_fields_store() -> &'static RwLock<HashMap<String, serde_json::Value>> {
47
+ MODEL_FIELDS.get_or_init(|| RwLock::new(HashMap::new()))
48
+ }
49
+
50
+ fn relation_map_store() -> &'static RwLock<HashMap<String, HashMap<String, RelationDef>>> {
51
+ RELATION_MAP.get_or_init(|| RwLock::new(HashMap::new()))
52
+ }
53
+
54
+ /// Store metadata provided by the generated client. Call once at startup.
55
+ pub fn set_adapter_metadata(meta: AdapterMetadata) {
56
+ if let Ok(mut store) = model_to_table_store().write() {
57
+ *store = meta.model_to_table;
58
+ }
59
+ if let Ok(mut store) = model_fields_store().write() {
60
+ *store = meta.model_fields;
61
+ }
62
+ if let Ok(mut store) = relation_map_store().write() {
63
+ *store = meta.relation_map;
64
+ }
65
+ }
66
+
67
+ /// Read-only snapshot of the model-to-table mapping.
68
+ pub fn get_model_to_table() -> HashMap<String, String> {
69
+ model_to_table_store()
70
+ .read()
71
+ .map(|s| s.clone())
72
+ .unwrap_or_default()
73
+ }
74
+
75
+ /// Field metadata for a model, if present.
76
+ pub fn get_fields_for_model(model: &str) -> Option<serde_json::Value> {
77
+ model_fields_store().read().ok().and_then(|s| s.get(model).cloned())
78
+ }
79
+
80
+ /// Relations declared for a model.
81
+ pub fn get_relations_for_model(model: &str) -> HashMap<String, RelationDef> {
82
+ relation_map_store()
83
+ .read()
84
+ .ok()
85
+ .and_then(|s| s.get(model).cloned())
86
+ .unwrap_or_default()
87
+ }
88
+
89
+ /// Resolve the physical table for a model, honouring overrides, then the
90
+ /// generated mapping, then a case-insensitive match, then the input itself.
91
+ pub fn resolve_table(model: &str) -> String {
92
+ let lower = model.to_lowercase();
93
+ // Overrides win over the generated mapping, and both match
94
+ // case-insensitively so `with_table("User", ..)` also covers `db.table("user")`.
95
+ if let Ok(store) = table_overrides_store().read() {
96
+ if let Some(t) = store.get(model) {
97
+ return t.clone();
98
+ }
99
+ for (k, v) in store.iter() {
100
+ if k.to_lowercase() == lower {
101
+ return v.clone();
102
+ }
103
+ }
104
+ }
105
+ if let Ok(store) = model_to_table_store().read() {
106
+ if let Some(t) = store.get(model) {
107
+ return t.clone();
108
+ }
109
+ for (k, v) in store.iter() {
110
+ if k.to_lowercase() == lower {
111
+ return v.clone();
112
+ }
113
+ }
114
+ }
115
+ model.to_string()
116
+ }
117
+
118
+ #[cfg(test)]
119
+ mod tests {
120
+ use super::*;
121
+
122
+ #[test]
123
+ fn resolves_table_with_case_fallback() {
124
+ let mut map = HashMap::new();
125
+ map.insert("User".to_string(), "[dbo].[Users]".to_string());
126
+ set_adapter_metadata(AdapterMetadata {
127
+ model_to_table: map,
128
+ ..Default::default()
129
+ });
130
+ assert_eq!(resolve_table("User"), "[dbo].[Users]");
131
+ assert_eq!(resolve_table("user"), "[dbo].[Users]");
132
+ assert_eq!(resolve_table("Missing"), "Missing");
133
+ }
134
+ }
@@ -0,0 +1,17 @@
1
+ //! Shared building blocks for the AN5 Rust adapter.
2
+ //!
3
+ //! Mirrors the Go `an5adapters/base` package: dialect handling, adapter
4
+ //! metadata and WHERE / ORDER BY construction.
5
+
6
+ pub mod dialect;
7
+ pub mod metadata;
8
+ pub mod where_builder;
9
+
10
+ pub use dialect::{
11
+ cosine_similarity, dot_product, euclidean_distance, vector_distance, Dialect,
12
+ };
13
+ pub use metadata::{
14
+ add_table_override, get_fields_for_model, get_model_to_table, get_relations_for_model,
15
+ resolve_table, set_adapter_metadata, AdapterMetadata, RelationDef,
16
+ };
17
+ pub use where_builder::{build_order_by, build_where, SqlArg, Where};