@an5/adapters 0.2.5 → 0.2.6

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 (82) hide show
  1. package/README.md +189 -305
  2. package/dist/an5Adapter.d.ts +85 -7
  3. package/dist/an5Adapter.js +736 -64
  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 +4 -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 +3 -1
  19. package/dist/index.js +7 -1
  20. package/dist/mssql/engine.js +8 -4
  21. package/dist/mysql/engine.js +1 -1
  22. package/dist/postgres/engine.js +1 -1
  23. package/dist/sqlite/browserEngine.d.ts +27 -0
  24. package/dist/sqlite/browserEngine.js +161 -0
  25. package/dist/sqlite/engine.js +15 -3
  26. package/dist/sqlite/index.d.ts +1 -0
  27. package/dist/sqlite/index.js +1 -0
  28. package/dist/typed.d.ts +5 -0
  29. package/dist/typed.js +2 -0
  30. package/dotnet/An5Adapter.cs +1089 -275
  31. package/dotnet/Base/Core.cs +340 -82
  32. package/dotnet/Mssql/MssqlEngine.cs +145 -144
  33. package/dotnet/Postgres/PostgresEngine.cs +121 -121
  34. package/golang/an5_adapter.go +314 -0
  35. package/golang/base/base.go +100 -0
  36. package/golang/base/metadata.go +116 -0
  37. package/golang/base/where.go +311 -0
  38. package/golang/go.mod +3 -0
  39. package/golang/mssql/mssql.go +32 -0
  40. package/golang/postgres/postgres.go +37 -0
  41. package/golang/sqlite/sqlite.go +14 -0
  42. package/golang/sqlite_test.go +42 -0
  43. package/golang/table_client.go +1329 -0
  44. package/package.json +17 -7
  45. package/python/an5_adapter.py +119 -97
  46. package/python/base/__init__.py +3 -3
  47. package/python/base/dialects.py +11 -8
  48. package/python/base/metadata.py +35 -10
  49. package/python/base/sql.py +172 -121
  50. package/python/mssql/__init__.py +1 -1
  51. package/python/mssql/provider.py +62 -62
  52. package/python/postgres/__init__.py +1 -1
  53. package/python/postgres/provider.py +15 -15
  54. package/python/table_client.py +619 -216
  55. package/typescript/src/an5Adapter.ts +1220 -505
  56. package/typescript/src/base/index.ts +3 -3
  57. package/typescript/src/base/metadata.ts +148 -28
  58. package/typescript/src/base/sql.ts +264 -98
  59. package/typescript/src/base/types.ts +27 -24
  60. package/typescript/src/browser.ts +10 -6
  61. package/typescript/src/config.ts +95 -95
  62. package/typescript/src/googlesheets/adapter.ts +269 -259
  63. package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
  64. package/typescript/src/googlesheets/config.ts +47 -47
  65. package/typescript/src/googlesheets/helpers.ts +144 -143
  66. package/typescript/src/googlesheets/index.ts +3 -3
  67. package/typescript/src/googlesheets/parseConnectionString.ts +48 -46
  68. package/typescript/src/googlesheets/retry.ts +126 -126
  69. package/typescript/src/googlesheets/sqlExecutor.ts +304 -301
  70. package/typescript/src/googlesheets/tableClient.ts +461 -455
  71. package/typescript/src/googlesheets/types.ts +6 -6
  72. package/typescript/src/index.ts +31 -27
  73. package/typescript/src/mssql/engine.ts +134 -132
  74. package/typescript/src/mssql/index.ts +1 -1
  75. package/typescript/src/mysql/engine.ts +80 -80
  76. package/typescript/src/mysql/index.ts +1 -1
  77. package/typescript/src/postgres/engine.ts +87 -87
  78. package/typescript/src/postgres/index.ts +1 -1
  79. package/typescript/src/sqlite/browserEngine.ts +192 -0
  80. package/typescript/src/sqlite/engine.ts +66 -55
  81. package/typescript/src/sqlite/index.ts +3 -1
  82. package/typescript/src/typed.ts +52 -0
@@ -1,121 +1,172 @@
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
-
1
+ from typing import Any, Dict, List, Optional
2
+ from .dialects import DIALECT_MSSQL, DIALECT_POSTGRES, DIALECT_SQLITE
3
+ from .metadata import model_to_table, model_fields
4
+
5
+ # ─── Quoting ───────────────────────────────────────────────────────────────────────
6
+
7
+ def _strip_wrapping(name: str, left: str, right: str) -> str:
8
+ return name[len(left):-len(right)] if name.startswith(left) and name.endswith(right) else name
9
+
10
+
11
+ def _quote(name: str, dialect: str) -> str:
12
+ raw = str(name)
13
+ if dialect in (DIALECT_POSTGRES, DIALECT_SQLITE):
14
+ unwrapped = _strip_wrapping(_strip_wrapping(raw, "[", "]"), '"', '"')
15
+ return f'"{unwrapped.replace(chr(34), chr(34) * 2)}"'
16
+ unwrapped = _strip_wrapping(raw, "[", "]")
17
+ return f"[{unwrapped.replace(']', ']]')}]"
18
+
19
+
20
+ def _sanitize_param_name(name: str) -> str:
21
+ cleaned = "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in str(name))
22
+ return cleaned if cleaned[:1].isalpha() else f"p_{cleaned}"
23
+
24
+
25
+ def _normalize_sort_direction(direction: Any) -> str:
26
+ return "DESC" if isinstance(direction, str) and direction.upper() == "DESC" else "ASC"
27
+
28
+
29
+ # ─── Connection helpers live in provider modules ────────────────────────────────
30
+
31
+ # ─── Where clause builder ──────────────────────────────────────────────────────────
32
+
33
+ _OPERATOR_KEYS = {"equals", "in", "notIn", "contains", "startsWith", "endsWith", "not", "gte", "lte", "gt", "lt"}
34
+
35
+
36
+ def _is_operator_value(value: Any) -> bool:
37
+ return isinstance(value, dict) and any(op in value for op in _OPERATOR_KEYS)
38
+
39
+
40
+ def _parse_where(model_name: str, where: Optional[Dict], params: Dict, dialect: str, prefix: str = "") -> str:
41
+ if not where:
42
+ return ""
43
+ conditions: List[str] = []
44
+
45
+ clean_where: Dict[str, Any] = {}
46
+ for key, value in where.items():
47
+ if "_" in key and isinstance(value, dict) and not _is_operator_value(value):
48
+ clean_where.update(value)
49
+ else:
50
+ clean_where[key] = value
51
+
52
+ for key, value in clean_where.items():
53
+ if key == "OR" and isinstance(value, list):
54
+ sub = [_parse_where(model_name, v, params, dialect, f"{prefix}or{i}_") for i, v in enumerate(value)]
55
+ sub = [s for s in sub if s]
56
+ if sub:
57
+ conditions.append(f"({' OR '.join(sub)})")
58
+ elif key == "AND":
59
+ items = value if isinstance(value, list) else [value]
60
+ sub = [_parse_where(model_name, v, params, dialect, f"{prefix}and{i}_") for i, v in enumerate(items)]
61
+ sub = [s for s in sub if s]
62
+ if sub:
63
+ conditions.append(f"({' AND '.join(sub)})")
64
+ elif key == "NOT":
65
+ items = value if isinstance(value, list) else [value]
66
+ sub = [_parse_where(model_name, v, params, dialect, f"{prefix}not{i}_") for i, v in enumerate(items)]
67
+ sub = [s for s in sub if s]
68
+ if sub:
69
+ conditions.append(f"NOT ({' AND '.join(sub)})")
70
+ else:
71
+ col = _quote(key, dialect)
72
+ pname = _sanitize_param_name(f"{prefix}{key}")
73
+ placeholder = "%s" if dialect == DIALECT_POSTGRES else "?"
74
+ if value is None:
75
+ conditions.append(f"{col} IS NULL")
76
+ elif isinstance(value, dict):
77
+ if "not" in value:
78
+ if value["not"] is None:
79
+ conditions.append(f"{col} IS NOT NULL")
80
+ elif isinstance(value["not"], dict):
81
+ nested_params: Dict[str, Any] = {}
82
+ nested_sql = _parse_where(model_name, {key: value["not"]}, nested_params, dialect, f"{prefix}{key}_not_")
83
+ params.update(nested_params)
84
+ if nested_sql:
85
+ conditions.append(f"NOT ({nested_sql})")
86
+ else:
87
+ params[f"{pname}_not"] = value["not"]
88
+ conditions.append(f"{col} <> {placeholder}")
89
+ if "equals" in value:
90
+ if value["equals"] is None:
91
+ conditions.append(f"{col} IS NULL")
92
+ else:
93
+ params[f"{pname}_eq"] = value["equals"]
94
+ conditions.append(f"{col} = {placeholder}")
95
+ if "contains" in value:
96
+ params[f"{pname}_co"] = f"%{value['contains']}%"
97
+ conditions.append(f"{col} LIKE {placeholder}")
98
+ if "startsWith" in value:
99
+ params[f"{pname}_sw"] = f"{value['startsWith']}%"
100
+ conditions.append(f"{col} LIKE {placeholder}")
101
+ if "endsWith" in value:
102
+ params[f"{pname}_ew"] = f"%{value['endsWith']}"
103
+ conditions.append(f"{col} LIKE {placeholder}")
104
+ if "gte" in value:
105
+ params[f"{pname}_gte"] = value["gte"]
106
+ conditions.append(f"{col} >= {placeholder}")
107
+ if "lte" in value:
108
+ params[f"{pname}_lte"] = value["lte"]
109
+ conditions.append(f"{col} <= {placeholder}")
110
+ if "gt" in value:
111
+ params[f"{pname}_gt"] = value["gt"]
112
+ conditions.append(f"{col} > {placeholder}")
113
+ if "lt" in value:
114
+ params[f"{pname}_lt"] = value["lt"]
115
+ conditions.append(f"{col} < {placeholder}")
116
+ if "in" in value:
117
+ vals = value["in"]
118
+ if vals:
119
+ placeholders = ", ".join([placeholder] * len(vals))
120
+ for i, v in enumerate(vals):
121
+ params[f"{pname}_in{i}"] = v
122
+ conditions.append(f"{col} IN ({placeholders})")
123
+ else:
124
+ conditions.append("1=0")
125
+ if "notIn" in value:
126
+ vals = value["notIn"]
127
+ if vals:
128
+ placeholders = ", ".join([placeholder] * len(vals))
129
+ for i, v in enumerate(vals):
130
+ params[f"{pname}_notin{i}"] = v
131
+ conditions.append(f"{col} NOT IN ({placeholders})")
132
+ else:
133
+ conditions.append("1=1")
134
+ else:
135
+ params[pname] = value
136
+ conditions.append(f"{col} = {placeholder}")
137
+
138
+ return " AND ".join(conditions)
139
+
140
+
141
+ def _build_order_by(order_by: Any, dialect: str) -> str:
142
+ if not order_by:
143
+ return ""
144
+ entries = order_by if isinstance(order_by, list) else [order_by]
145
+ parts: List[str] = []
146
+ for entry in entries:
147
+ for col, direction in entry.items():
148
+ parts.append(f"{_quote(col, dialect)} {_normalize_sort_direction(direction)}")
149
+ return f"ORDER BY {', '.join(parts)}" if parts else ""
150
+
151
+
152
+ # ─── Table name resolution ─────────────────────────────────────────────────────────
153
+
154
+ def _resolve_table(model_name: str) -> str:
155
+ if model_name in model_to_table:
156
+ return model_to_table[model_name]
157
+ camel = model_name[0].lower() + model_name[1:] if model_name else model_name
158
+ if camel in model_to_table:
159
+ return model_to_table[camel]
160
+ lower = model_name.lower()
161
+ if lower in model_to_table:
162
+ return model_to_table[lower]
163
+ return model_name
164
+
165
+
166
+ def _quote_table(t: str, dialect: str) -> str:
167
+ if t.startswith("[") or t.startswith('"'):
168
+ return t
169
+ if "." in t:
170
+ return ".".join(_quote(p, dialect) for p in t.split("."))
171
+ return _quote(t, dialect)
172
+
@@ -1 +1 @@
1
- from .provider import PYODBC_AVAILABLE, connect, parse_connection_string, placeholder
1
+ from .provider import PYODBC_AVAILABLE, connect, parse_connection_string, placeholder
@@ -1,62 +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 "?"
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 "?"
@@ -1 +1 @@
1
- from .provider import PSYCOPG2_AVAILABLE, connect, placeholder
1
+ from .provider import PSYCOPG2_AVAILABLE, connect, placeholder
@@ -1,15 +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"
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"