@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@an5/adapters",
3
- "version": "0.2.5",
4
- "description": "Standalone language adapters for AN5 ORM - TypeScript, Python, .NET query execution.",
3
+ "version": "0.2.9",
4
+ "description": "Standalone language adapters for AN5 ORM - TypeScript, Python, .NET, Go, Rust, and Google Sheets query execution.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -20,7 +20,10 @@
20
20
  "./typescript": "./dist/index.js",
21
21
  "./typescript/*": "./dist/*",
22
22
  "./python": "./python/an5_adapter.py",
23
- "./dotnet": "./dotnet/An5Adapter.cs"
23
+ "./dotnet": "./dotnet/An5Adapter.cs",
24
+ "./golang": "./golang/an5_adapter.go",
25
+ "./rust": "./rust/Cargo.toml",
26
+ "./nbase": "./dist/nbase/index.js"
24
27
  },
25
28
  "author": "an5ORM",
26
29
  "homepage": "https://github.com/an5ORM/an5",
@@ -34,20 +37,34 @@
34
37
  "adapter",
35
38
  "typescript",
36
39
  "python",
37
- "dotnet"
40
+ "dotnet",
41
+ "go",
42
+ "golang",
43
+ "rust"
38
44
  ],
39
45
  "files": [
40
46
  "dist/**/*.{js,d.ts}",
41
47
  "typescript/src/**/*.ts",
42
48
  "python/**/*.py",
43
49
  "dotnet/**/*",
44
- "README.md"
50
+ "golang/**/*",
51
+ "rust/Cargo.toml",
52
+ "rust/src/**/*",
53
+ "README.md",
54
+ "typescript/src/nbase/**/*.ts"
45
55
  ],
46
56
  "scripts": {
47
57
  "build": "tsc -p tsconfig.json",
48
- "build:python": "python -m build --outdir dist-py",
49
- "test": "node test/unit.test.js && node test/package.test.js",
50
- "test:python": "python -m compileall python 2>&1"
58
+ "pretest": "npm run build",
59
+ "prepack": "npm run build",
60
+ "build:python": "(python -m build --outdir dist-py || python3 -m build --outdir dist-py)",
61
+ "test": "node test/unit.test.js && node test/package.test.js && node test/nbase.test.js && node --test test/version-sync.test.js",
62
+ "test:package:smoke": "node scripts/package-smoke.js",
63
+ "test:integration:live": "node test/live-db.integration.test.js",
64
+ "test:python": "(python -m compileall python 2>&1 || python3 -m compileall python 2>&1)",
65
+ "test:dotnet": "node scripts/dotnet-compile-check.js && node scripts/dotnet-sqlite-smoke.js",
66
+ "test:go": "cd golang && go test ./...",
67
+ "test:rust": "node scripts/rust-compile-check.js"
51
68
  },
52
69
  "dependencies": {
53
70
  "googleapis": "^134.0.0",
@@ -58,7 +75,7 @@
58
75
  "@types/mssql": "^9.1.8",
59
76
  "@types/node": "^20.11.0",
60
77
  "@types/pg": "^8.11.0",
61
- "typescript": "^6.0.3"
78
+ "typescript": "^7.0.2"
62
79
  },
63
80
  "publishConfig": {
64
81
  "access": "public"
@@ -1,97 +1,184 @@
1
- """Standalone Python runtime adapter for AN5 ORM."""
2
-
3
- from typing import Dict, List, Optional
4
-
5
- try:
6
- from .base import DIALECT_MSSQL, DIALECT_POSTGRES, detect_dialect, set_adapter_metadata
7
- from .mssql import connect as connect_mssql
8
- from .postgres import connect as connect_postgres
9
- from .table_client import AdapterTableClient
10
- except ImportError:
11
- from base import DIALECT_MSSQL, DIALECT_POSTGRES, detect_dialect, set_adapter_metadata
12
- from mssql import connect as connect_mssql
13
- from postgres import connect as connect_postgres
14
- from table_client import AdapterTableClient
15
-
16
- # Backward-compatible aliases used by tests and older imports.
17
- _detect_dialect = detect_dialect
18
- try:
19
- from .mssql import parse_connection_string as _parse_connection_string
20
- except ImportError:
21
- from mssql import parse_connection_string as _parse_connection_string
22
-
23
- class An5Adapter:
24
- def __init__(self, connection_string: str):
25
- self._dialect = detect_dialect(connection_string)
26
- self._conn_str = connection_string
27
-
28
- def _connect(self):
29
- if self._dialect == DIALECT_POSTGRES:
30
- return connect_postgres(self._conn_str)
31
- return connect_mssql(self._conn_str)
32
-
33
- def _to_dicts(self, cursor, query: str) -> List[Dict]:
34
- if cursor.description:
35
- cols = [col[0] for col in cursor.description]
36
- rows = cursor.fetchall() if cursor.description else []
37
- return [dict(zip(cols, row)) for row in rows]
38
- return []
39
-
40
- def exec(self, query: str, params: Optional[List] = None) -> List[Dict]:
41
- conn = self._connect()
42
- try:
43
- cursor = conn.cursor()
44
- cursor.execute(query, params or [])
45
- return self._to_dicts(cursor, query)
46
- finally:
47
- conn.close()
48
-
49
- def execute(self, query: str, params: Optional[List] = None) -> int:
50
- conn = self._connect()
51
- try:
52
- cursor = conn.cursor()
53
- cursor.execute(query, params or [])
54
- return cursor.rowcount
55
- finally:
56
- conn.close()
57
-
58
- def query_raw(self, query: str, *values) -> List[Dict]:
59
- return self.exec(query, list(values))
60
-
61
- def execute_raw(self, query: str, *values) -> int:
62
- return self.execute(query, list(values))
63
-
64
- def table(self, model_name: str) -> AdapterTableClient:
65
- return AdapterTableClient(self, model_name)
66
-
67
- def __getattr__(self, model_name: str) -> AdapterTableClient:
68
- return self.table(model_name)
69
-
70
- def transaction(self, fn):
71
- conn = self._connect()
72
- conn.autocommit = False
73
- try:
74
- result = fn(self)
75
- conn.commit()
76
- return result
77
- except Exception:
78
- conn.rollback()
79
- raise
80
- finally:
81
- if self._dialect == DIALECT_POSTGRES:
82
- conn.autocommit = True
83
- conn.close()
84
-
85
- def create_an5_adapter(connection_string: str) -> An5Adapter:
86
- return An5Adapter(connection_string)
87
-
88
- __all__ = [
89
- "An5Adapter",
90
- "AdapterTableClient",
91
- "create_an5_adapter",
92
- "DIALECT_MSSQL",
93
- "DIALECT_POSTGRES",
94
- "_detect_dialect",
95
- "_parse_connection_string",
96
- "set_adapter_metadata",
97
- ]
1
+ """Standalone Python runtime adapter for AN5 ORM."""
2
+
3
+ from typing import Dict, List, Optional, Any
4
+
5
+ try:
6
+ from .base import DIALECT_MSSQL, DIALECT_POSTGRES, DIALECT_SQLITE, detect_dialect, set_adapter_metadata
7
+ from .mssql import connect as connect_mssql
8
+ from .postgres import connect as connect_postgres
9
+ from .sqlite import connect as connect_sqlite, is_memory as is_sqlite_memory
10
+ from .table_client import AdapterTableClient, ViewClient
11
+ except ImportError:
12
+ from base import DIALECT_MSSQL, DIALECT_POSTGRES, DIALECT_SQLITE, detect_dialect, set_adapter_metadata
13
+ from mssql import connect as connect_mssql
14
+ from postgres import connect as connect_postgres
15
+ from sqlite import connect as connect_sqlite, is_memory as is_sqlite_memory
16
+ from table_client import AdapterTableClient, ViewClient
17
+
18
+ # Backward-compatible aliases used by tests and older imports.
19
+ _detect_dialect = detect_dialect
20
+ try:
21
+ from .mssql import parse_connection_string as _parse_connection_string
22
+ except ImportError:
23
+ from mssql import parse_connection_string as _parse_connection_string
24
+
25
+ class An5Adapter:
26
+ def __init__(self, connection_string: str):
27
+ self._dialect = detect_dialect(connection_string)
28
+ self._conn_str = connection_string
29
+ # The open connection of `transaction()`, when there is one. Every
30
+ # statement in the callback has to use this very connection, otherwise a
31
+ # rollback returns to a different, already committed connection — that
32
+ # is "the error was raised but the data is still there".
33
+ self._txn_conn = None
34
+ # Long-lived connection for in-memory databases, because `:memory:` is
35
+ # gone as soon as every connection is closed.
36
+ self._conn = None
37
+
38
+ def _connect(self):
39
+ if self._dialect == DIALECT_POSTGRES:
40
+ return connect_postgres(self._conn_str)
41
+ if self._dialect == DIALECT_SQLITE:
42
+ return connect_sqlite(self._conn_str)
43
+ return connect_mssql(self._conn_str)
44
+
45
+ def _acquire(self):
46
+ """Returns (connection, whether we opened it and must close it)."""
47
+ if self._txn_conn is not None:
48
+ return self._txn_conn, False
49
+ if self._dialect == DIALECT_SQLITE and is_sqlite_memory(self._conn_str):
50
+ if self._conn is None:
51
+ self._conn = self._connect()
52
+ return self._conn, False
53
+ return self._connect(), True
54
+
55
+ def close(self):
56
+ """Closes the long-lived connection (in-memory databases only)."""
57
+ if self._conn is not None:
58
+ self._conn.close()
59
+ self._conn = None
60
+
61
+ def _to_dicts(self, cursor, query: str) -> List[Dict]:
62
+ if cursor.description:
63
+ cols = [col[0] for col in cursor.description]
64
+ rows = cursor.fetchall() if cursor.description else []
65
+ return [dict(zip(cols, row)) for row in rows]
66
+ return []
67
+
68
+ def exec(self, query: str, params: Optional[List] = None) -> List[Dict]:
69
+ conn, owned = self._acquire()
70
+ try:
71
+ cursor = conn.cursor()
72
+ cursor.execute(query, params or [])
73
+ return self._to_dicts(cursor, query)
74
+ finally:
75
+ if owned:
76
+ conn.close()
77
+
78
+ def execute(self, query: str, params: Optional[List] = None) -> int:
79
+ conn, owned = self._acquire()
80
+ try:
81
+ cursor = conn.cursor()
82
+ cursor.execute(query, params or [])
83
+ return cursor.rowcount
84
+ finally:
85
+ if owned:
86
+ conn.close()
87
+
88
+ def query_raw(self, query: str, *values) -> List[Dict]:
89
+ return self.exec(query, list(values))
90
+
91
+ def execute_raw(self, query: str, *values) -> int:
92
+ return self.execute(query, list(values))
93
+
94
+ def table(self, model_name: str) -> AdapterTableClient[Any]:
95
+ return AdapterTableClient(self, model_name)
96
+
97
+ def view(self, view_name: str) -> ViewClient:
98
+ return ViewClient(self, view_name)
99
+
100
+ def query_proc(self, proc_name: str, params: Optional[List] = None) -> List[Dict]:
101
+ if self._dialect == DIALECT_SQLITE:
102
+ # SQLite has no stored procedures. This used to fall through to the
103
+ # MSSQL branch and generate `EXEC ...`, which is not valid SQL, and
104
+ # the error only surfaced at the call. Saying so beats sending wrong
105
+ # SQL.
106
+ raise NotImplementedError(
107
+ "SQLite has no stored procedures. Use the generated table client "
108
+ "(db.<model>.*) or db.exec() with a parameterised query."
109
+ )
110
+ if self._dialect == DIALECT_POSTGRES:
111
+ placeholders = ", ".join(["%s"] * len(params or []))
112
+ sql = f"CALL {proc_name}({placeholders})" if placeholders else f"CALL {proc_name}()"
113
+ else:
114
+ placeholders = ", ".join(["?"] * len(params or []))
115
+ sql = f"EXEC {proc_name} {placeholders}" if placeholders else f"EXEC {proc_name}"
116
+ return self.exec(sql, params or [])
117
+
118
+ def execute_proc(self, proc_name: str, params: Optional[List] = None) -> int:
119
+ if self._dialect == DIALECT_SQLITE:
120
+ raise NotImplementedError(
121
+ "SQLite has no stored procedures. Use the generated table client "
122
+ "(db.<model>.*) or db.execute() with a parameterised query."
123
+ )
124
+ if self._dialect == DIALECT_POSTGRES:
125
+ placeholders = ", ".join(["%s"] * len(params or []))
126
+ sql = f"CALL {proc_name}({placeholders})" if placeholders else f"CALL {proc_name}()"
127
+ else:
128
+ placeholders = ", ".join(["?"] * len(params or []))
129
+ sql = f"EXEC {proc_name} {placeholders}" if placeholders else f"EXEC {proc_name}"
130
+ return self.execute(sql, params or [])
131
+
132
+ def __getattr__(self, model_name: str) -> AdapterTableClient:
133
+ return self.table(model_name)
134
+
135
+ def transaction(self, fn):
136
+ if self._txn_conn is not None:
137
+ raise RuntimeError("Nested transaction() is not supported")
138
+ # In-memory database: the connection holding the data has to be reused.
139
+ # Opening a new one here points at a different, empty database — the
140
+ # table just created disappears and every statement in the transaction
141
+ # fails.
142
+ conn = self._conn if self._conn is not None else self._connect()
143
+ owned = self._conn is None
144
+ if self._dialect == DIALECT_SQLITE:
145
+ # `sqlite3.Connection` has no `autocommit` attribute; transactions
146
+ # are driven by `isolation_level`. The provider opens at `None`
147
+ # (autocommit), so to get a transaction it has to be set back to `""`
148
+ # and sqlite3 opens an implicit BEGIN before each DML statement —
149
+ # only then do `commit()`/`rollback()` do anything.
150
+ conn.isolation_level = ""
151
+ else:
152
+ conn.autocommit = False
153
+ self._txn_conn = conn
154
+ try:
155
+ result = fn(self)
156
+ conn.commit()
157
+ return result
158
+ except Exception:
159
+ conn.rollback()
160
+ raise
161
+ finally:
162
+ self._txn_conn = None
163
+ if self._dialect == DIALECT_POSTGRES:
164
+ conn.autocommit = True
165
+ elif self._dialect == DIALECT_SQLITE:
166
+ conn.isolation_level = None
167
+ if owned:
168
+ conn.close()
169
+
170
+ def create_an5_adapter(connection_string: str) -> An5Adapter:
171
+ return An5Adapter(connection_string)
172
+
173
+ __all__ = [
174
+ "An5Adapter",
175
+ "AdapterTableClient",
176
+ "ViewClient",
177
+ "create_an5_adapter",
178
+ "DIALECT_MSSQL",
179
+ "DIALECT_POSTGRES",
180
+ "DIALECT_SQLITE",
181
+ "_detect_dialect",
182
+ "_parse_connection_string",
183
+ "set_adapter_metadata",
184
+ ]
@@ -1,3 +1,3 @@
1
- from .dialects import DIALECT_MSSQL, DIALECT_POSTGRES, detect_dialect
2
- from .metadata import model_to_table, model_fields, set_adapter_metadata
3
- from .sql import _quote, _parse_where, _build_order_by, _resolve_table, _quote_table
1
+ from .dialects import DIALECT_MSSQL, DIALECT_POSTGRES, DIALECT_SQLITE, detect_dialect
2
+ from .metadata import model_to_table, model_fields, relation_map, set_adapter_metadata, get_relations_for_model, get_model_to_table, get_fields_for_model
3
+ from .sql import _quote, _parse_where, _build_order_by, _resolve_table, _quote_table
@@ -1,8 +1,11 @@
1
- DIALECT_MSSQL = "mssql"
2
- DIALECT_POSTGRES = "postgres"
3
-
4
- def detect_dialect(connection_string: str) -> str:
5
- cs = connection_string.strip().lower()
6
- if cs.startswith("postgres://") or cs.startswith("postgresql://"):
7
- return DIALECT_POSTGRES
8
- return DIALECT_MSSQL
1
+ DIALECT_MSSQL = "mssql"
2
+ DIALECT_POSTGRES = "postgres"
3
+ DIALECT_SQLITE = "sqlite"
4
+
5
+ def detect_dialect(connection_string: str) -> str:
6
+ cs = connection_string.strip().lower()
7
+ if cs.startswith("postgres://") or cs.startswith("postgresql://"):
8
+ return DIALECT_POSTGRES
9
+ if cs.startswith("sqlite://") or cs.startswith("sqlite:") or cs.startswith("file:") or cs.endswith(".db") or cs.endswith(".sqlite") or cs.endswith(".sqlite3") or cs == ":memory:":
10
+ return DIALECT_SQLITE
11
+ return DIALECT_MSSQL
@@ -1,10 +1,59 @@
1
- from typing import Any, Dict
2
-
3
- model_to_table: Dict[str, str] = {}
4
- model_fields: Dict[str, Any] = {}
5
-
6
- def set_adapter_metadata(metadata: Dict[str, Any]) -> None:
7
- model_to_table.clear()
8
- model_to_table.update(metadata.get("model_to_table") or metadata.get("modelToTable") or {})
9
- model_fields.clear()
10
- model_fields.update(metadata.get("model_fields") or metadata.get("modelFields") or {})
1
+ from typing import Any, Dict, List, Optional
2
+
3
+ model_to_table: Dict[str, str] = {}
4
+ model_fields: Dict[str, Any] = {}
5
+ relation_map: Dict[str, Dict[str, Dict[str, str]]] = {}
6
+
7
+ def set_adapter_metadata(metadata: Dict[str, Any]) -> None:
8
+ model_to_table.clear()
9
+ model_to_table.update(metadata.get("model_to_table") or metadata.get("modelToTable") or {})
10
+ model_fields.clear()
11
+ model_fields.update(metadata.get("model_fields") or metadata.get("modelFields") or {})
12
+ relation_map.clear()
13
+ rels = metadata.get("relation_map") or metadata.get("relationMap") or metadata.get("RELATION_MAP") or {}
14
+ if isinstance(rels, dict):
15
+ for model, relations in rels.items():
16
+ if not isinstance(relations, dict):
17
+ continue
18
+ relation_map[model] = {}
19
+ for key, defn in relations.items():
20
+ if isinstance(defn, dict):
21
+ relation_map[model][key] = {
22
+ "modelName": defn.get("modelName") or defn.get("model_name") or "",
23
+ "relationType": defn.get("relationType") or defn.get("relation_type") or "many",
24
+ "foreignKey": defn.get("foreignKey") or defn.get("foreign_key") or "",
25
+ "localKey": defn.get("localKey") or defn.get("local_key") or "",
26
+ }
27
+
28
+ def resolve_model_key(model_name: str) -> str:
29
+ """The model name in the metadata, accepting a different capitalisation.
30
+
31
+ The generated client registers tables under their PascalCase name
32
+ (`CatalogType`) while the metadata keys them camelCase/snake_case
33
+ (`catalogType`, `catalog_type`). A plain lookup would find no columns at all
34
+ and silently skip `isId` — that is, never generate the primary key.
35
+ """
36
+ if not model_name:
37
+ return model_name
38
+ if model_name in model_to_table or model_name in model_fields:
39
+ return model_name
40
+ camel = model_name[0].lower() + model_name[1:]
41
+ if camel in model_to_table or camel in model_fields:
42
+ return camel
43
+ lower = model_name.lower()
44
+ if lower in model_to_table or lower in model_fields:
45
+ return lower
46
+ return model_name
47
+
48
+
49
+ def get_relations_for_model(model_name: str) -> Dict[str, Dict[str, str]]:
50
+ return relation_map.get(model_name) or relation_map.get(resolve_model_key(model_name)) or {}
51
+
52
+ def get_model_to_table() -> Dict[str, str]:
53
+ return dict(model_to_table)
54
+
55
+ def get_fields_for_model(model_name: str) -> Any:
56
+ key = resolve_model_key(model_name)
57
+ if key in model_fields:
58
+ return model_fields[key]
59
+ return model_fields.get(model_name)