singularitysql 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,122 @@
1
+ """Introspection strategy for Azure SQL Database.
2
+
3
+ Uses sys.parameters for parameter metadata and
4
+ sys.dm_exec_describe_first_result_set for result set column metadata.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from singularity.exceptions import SpNotFoundError
12
+ from singularity.types import ColumnInfo, Parameter, SPMetadata
13
+ from singularity.version._base import VersionIntrospector
14
+
15
+
16
+ class AzureIntrospector(VersionIntrospector):
17
+ """Strategy for Azure SQL Database: sys.parameters + sys.dm_exec_describe_first_result_set."""
18
+
19
+ def introspect(self, sp_name: str, cursor: Any) -> SPMetadata:
20
+ """Introspect a stored procedure on Azure SQL Database.
21
+
22
+ Args:
23
+ sp_name: The stored procedure name.
24
+ cursor: An active pyodbc Cursor.
25
+
26
+ Returns:
27
+ SPMetadata with parameters and result set columns.
28
+
29
+ Raises:
30
+ SpNotFoundError: If the procedure does not exist.
31
+ """
32
+ simple_name = sp_name.split(".")[-1] if "." in sp_name else sp_name
33
+
34
+ # Verify the procedure exists
35
+ cursor.execute(
36
+ """
37
+ SELECT COUNT(*)
38
+ FROM sys.objects
39
+ WHERE type = 'P'
40
+ AND name = ?
41
+ """,
42
+ (simple_name,),
43
+ )
44
+ if cursor.fetchone()[0] == 0:
45
+ raise SpNotFoundError(sp_name)
46
+
47
+ # Fetch parameter metadata from sys.parameters
48
+ cursor.execute(
49
+ """
50
+ SELECT
51
+ p.name,
52
+ tp.name AS data_type,
53
+ CASE
54
+ WHEN p.is_output = 1 THEN 'OUT'
55
+ ELSE 'IN'
56
+ END AS param_mode,
57
+ p.has_default_value,
58
+ p.is_nullable
59
+ FROM sys.parameters p
60
+ JOIN sys.types tp ON p.user_type_id = tp.user_type_id
61
+ JOIN sys.objects o ON p.object_id = o.object_id
62
+ WHERE o.type = 'P'
63
+ AND o.name = ?
64
+ ORDER BY p.parameter_id
65
+ """,
66
+ (simple_name,),
67
+ )
68
+
69
+ parameters: list[Parameter] = []
70
+ for row in cursor.fetchall():
71
+ raw_name: str = row[0]
72
+ sql_type: str = row[1]
73
+ direction: str = row[2]
74
+ has_default: bool = row[3] or False
75
+ nullable: bool = row[4] or False
76
+
77
+ parameters.append(
78
+ Parameter(
79
+ name=raw_name,
80
+ sql_type=sql_type.upper(),
81
+ direction=direction, # type: ignore[arg-type]
82
+ default=None if not has_default else "(default)",
83
+ nullable=nullable,
84
+ )
85
+ )
86
+
87
+ # Fetch result sets using Azure's DMV (first result set only)
88
+ result_sets: list[list[ColumnInfo]] = []
89
+ try:
90
+ cursor.execute(
91
+ """
92
+ SELECT
93
+ name,
94
+ system_type_name,
95
+ is_nullable
96
+ FROM sys.dm_exec_describe_first_result_set(
97
+ N'EXEC ' + QUOTENAME(?), NULL, 0
98
+ )
99
+ """,
100
+ (simple_name,),
101
+ )
102
+ columns = [
103
+ ColumnInfo(
104
+ name=row[0], # name
105
+ sql_type=(row[1] or "UNKNOWN").upper(), # system_type_name
106
+ nullable=bool(row[2]), # is_nullable
107
+ )
108
+ for row in cursor.fetchall()
109
+ if row[0] is not None
110
+ ]
111
+ if columns:
112
+ result_sets.append(columns)
113
+ except Exception:
114
+ pass
115
+
116
+ # Enrich with column descriptions from extended properties
117
+ for rs in result_sets:
118
+ descriptions = self._fetch_column_descriptions(sp_name, cursor, rs)
119
+ for col in rs:
120
+ col.description = descriptions.get(col.name)
121
+
122
+ return SPMetadata(name=sp_name, parameters=parameters, result_sets=result_sets)
@@ -0,0 +1,73 @@
1
+ """Abstract base class for version-specific introspection strategies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any
7
+
8
+ from singularity.types import SPMetadata
9
+
10
+
11
+ class VersionIntrospector(ABC):
12
+ """Strategy for introspecting stored procedure metadata for a specific
13
+ SQL Server version or platform.
14
+
15
+ Each concrete implementation knows the correct system views, functions,
16
+ and DMVs to query for parameter and result set metadata.
17
+ """
18
+
19
+ @abstractmethod
20
+ def introspect(self, sp_name: str, cursor: Any) -> SPMetadata:
21
+ """Introspect a stored procedure and return its metadata.
22
+
23
+ Args:
24
+ sp_name: The stored procedure name (with or without schema).
25
+ cursor: An active pyodbc Cursor connected to the database.
26
+
27
+ Returns:
28
+ SPMetadata containing parameters and result set column info.
29
+
30
+ Raises:
31
+ SpNotFoundError: If the stored procedure does not exist.
32
+ """
33
+ ...
34
+
35
+ def _fetch_column_descriptions(
36
+ self, sp_name: str, cursor: Any, columns: list[Any],
37
+ ) -> dict[str, str]:
38
+ """Query sys.extended_properties for column descriptions.
39
+
40
+ Args:
41
+ sp_name: The stored procedure name (schema-qualified if available).
42
+ cursor: An active pyodbc Cursor.
43
+ columns: List of ColumnInfo objects to look up descriptions for.
44
+
45
+ Returns:
46
+ A dict mapping column names to their descriptions.
47
+ """
48
+ if not columns:
49
+ return {}
50
+
51
+ descriptions: dict[str, str] = {}
52
+
53
+ try:
54
+ for col in columns:
55
+ cursor.execute(
56
+ """
57
+ SELECT CAST(ep.value AS NVARCHAR(MAX))
58
+ FROM sys.extended_properties ep
59
+ WHERE ep.class = 1
60
+ AND ep.major_id = OBJECT_ID(?)
61
+ AND ep.minor_id = COLUMNPROPERTY(OBJECT_ID(?), ?, 'ColumnId')
62
+ AND ep.name = 'MS_Description'
63
+ """,
64
+ (sp_name, sp_name, col.name),
65
+ )
66
+ row = cursor.fetchone()
67
+ if row and row[0] is not None:
68
+ descriptions[col.name] = str(row[0])
69
+ except Exception:
70
+ # Extended properties are optional — silently skip on failure
71
+ pass
72
+
73
+ return descriptions
@@ -0,0 +1,150 @@
1
+ """Introspection strategy for SQL Server 2008–2014.
2
+
3
+ Uses sys.parameters for parameter metadata and falls back to
4
+ sys.columns + sys.objects for result set columns when
5
+ sp_describe_first_result_set is unavailable.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from singularity.exceptions import SpNotFoundError
13
+ from singularity.types import ColumnInfo, Parameter, SPMetadata
14
+ from singularity.version._base import VersionIntrospector
15
+
16
+
17
+ class LegacyIntrospector(VersionIntrospector):
18
+ """Strategy for SQL Server 2008–2014 using sys.parameters + sys.columns/sys.objects fallback."""
19
+
20
+ def introspect(self, sp_name: str, cursor: Any) -> SPMetadata:
21
+ """Introspect a stored procedure using legacy SQL Server system views.
22
+
23
+ Args:
24
+ sp_name: The stored procedure name.
25
+ cursor: An active pyodbc Cursor.
26
+
27
+ Returns:
28
+ SPMetadata with parameters and result set columns.
29
+
30
+ Raises:
31
+ SpNotFoundError: If the procedure does not exist.
32
+ """
33
+ simple_name = sp_name.split(".")[-1] if "." in sp_name else sp_name
34
+
35
+ # Verify the procedure exists via sys.objects
36
+ cursor.execute(
37
+ """
38
+ SELECT COUNT(*)
39
+ FROM sys.objects
40
+ WHERE type = 'P'
41
+ AND name = ?
42
+ """,
43
+ (simple_name,),
44
+ )
45
+ if cursor.fetchone()[0] == 0:
46
+ raise SpNotFoundError(sp_name)
47
+
48
+ # Fetch parameter metadata from sys.parameters
49
+ cursor.execute(
50
+ """
51
+ SELECT
52
+ p.name,
53
+ tp.name AS data_type,
54
+ CASE
55
+ WHEN p.is_output = 1 THEN 'OUT'
56
+ ELSE 'IN'
57
+ END AS param_mode,
58
+ p.has_default_value,
59
+ p.is_nullable
60
+ FROM sys.parameters p
61
+ JOIN sys.types tp ON p.user_type_id = tp.user_type_id
62
+ JOIN sys.objects o ON p.object_id = o.object_id
63
+ WHERE o.type = 'P'
64
+ AND o.name = ?
65
+ ORDER BY p.parameter_id
66
+ """,
67
+ (simple_name,),
68
+ )
69
+
70
+ parameters: list[Parameter] = []
71
+ for row in cursor.fetchall():
72
+ raw_name: str = row[0] # e.g. "@OrderId"
73
+ sql_type: str = row[1]
74
+ direction: str = row[2]
75
+ has_default: bool = row[3] or False
76
+ nullable: bool = row[4] or False
77
+
78
+ parameters.append(
79
+ Parameter(
80
+ name=raw_name,
81
+ sql_type=sql_type.upper(),
82
+ direction=direction, # type: ignore[arg-type]
83
+ default=None if not has_default else "(default)",
84
+ nullable=nullable,
85
+ )
86
+ )
87
+
88
+ # First result set: try sp_describe_first_result_set;
89
+ # fall back to sys.columns + sys.objects join.
90
+ result_sets: list[list[ColumnInfo]] = []
91
+
92
+ try:
93
+ tsql = f"EXEC {sp_name}"
94
+ for idx in range(10):
95
+ cursor.execute(
96
+ "{CALL sp_describe_first_result_set(?, NULL, ?)}",
97
+ (tsql, idx),
98
+ )
99
+ rows = cursor.fetchall()
100
+ columns = [
101
+ ColumnInfo(
102
+ name=row[2], # name (index 2)
103
+ sql_type=(row[5] or "UNKNOWN").upper(), # system_type_name (index 5)
104
+ nullable=bool(row[3]), # is_nullable (index 3)
105
+ )
106
+ for row in rows
107
+ if row[2] is not None
108
+ ]
109
+ if not columns:
110
+ break
111
+ result_sets.append(columns)
112
+ except Exception:
113
+ # Fallback: query sys.columns through sys.objects for the SP
114
+ try:
115
+ cursor.execute(
116
+ """
117
+ SELECT
118
+ c.name,
119
+ tp.name AS data_type,
120
+ c.is_nullable
121
+ FROM sys.columns c
122
+ JOIN sys.objects o ON c.object_id = o.object_id
123
+ JOIN sys.types tp ON c.user_type_id = tp.user_type_id
124
+ WHERE o.type = 'P'
125
+ AND o.name = ?
126
+ ORDER BY c.column_id
127
+ """,
128
+ (simple_name,),
129
+ )
130
+ columns = [
131
+ ColumnInfo(
132
+ name=row[0],
133
+ sql_type=row[1].upper(),
134
+ nullable=row[2] or False,
135
+ )
136
+ for row in cursor.fetchall()
137
+ ]
138
+ if columns:
139
+ result_sets.append(columns)
140
+ except Exception:
141
+ # If everything fails, return empty result sets gracefully
142
+ pass
143
+
144
+ # Enrich with column descriptions from extended properties
145
+ for rs in result_sets:
146
+ descriptions = self._fetch_column_descriptions(sp_name, cursor, rs)
147
+ for col in rs:
148
+ col.description = descriptions.get(col.name)
149
+
150
+ return SPMetadata(name=sp_name, parameters=parameters, result_sets=result_sets)
@@ -0,0 +1,122 @@
1
+ """Introspection strategy for SQL Server 2016 and later.
2
+
3
+ Uses INFORMATION_SCHEMA.PARAMETERS for parameter metadata and
4
+ sp_describe_first_result_set for result set column metadata.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from singularity.exceptions import SpNotFoundError
12
+ from singularity.types import ColumnInfo, Parameter, SPMetadata
13
+ from singularity.version._base import VersionIntrospector
14
+
15
+
16
+ class ModernIntrospector(VersionIntrospector):
17
+ """Strategy for SQL Server 2016+ using INFORMATION_SCHEMA + sp_describe_first_result_set."""
18
+
19
+ def introspect(self, sp_name: str, cursor: Any) -> SPMetadata:
20
+ """Introspect a stored procedure using modern SQL Server system views.
21
+
22
+ Args:
23
+ sp_name: The stored procedure name.
24
+ cursor: An active pyodbc Cursor.
25
+
26
+ Returns:
27
+ SPMetadata with parameters and result set columns.
28
+
29
+ Raises:
30
+ SpNotFoundError: If the procedure does not exist.
31
+ """
32
+ # Strip schema prefix for lookup if present
33
+ simple_name = sp_name.split(".")[-1] if "." in sp_name else sp_name
34
+
35
+ # Verify the procedure exists
36
+ cursor.execute(
37
+ """
38
+ SELECT COUNT(*)
39
+ FROM INFORMATION_SCHEMA.ROUTINES
40
+ WHERE ROUTINE_TYPE = 'PROCEDURE'
41
+ AND ROUTINE_NAME = ?
42
+ """,
43
+ (simple_name,),
44
+ )
45
+ if cursor.fetchone()[0] == 0:
46
+ raise SpNotFoundError(sp_name)
47
+
48
+ # Fetch parameter metadata from sys.parameters
49
+ cursor.execute(
50
+ """
51
+ SELECT
52
+ p.name,
53
+ tp.name AS data_type,
54
+ CASE
55
+ WHEN p.is_output = 1 THEN 'OUT'
56
+ WHEN p.is_output = 0 AND p.is_readonly = 0 THEN 'IN'
57
+ ELSE 'INOUT'
58
+ END AS param_mode,
59
+ p.has_default_value,
60
+ p.is_nullable
61
+ FROM sys.parameters p
62
+ JOIN sys.types tp ON p.user_type_id = tp.user_type_id
63
+ JOIN sys.objects o ON p.object_id = o.object_id
64
+ WHERE o.type = 'P'
65
+ AND o.name = ?
66
+ ORDER BY p.parameter_id
67
+ """,
68
+ (simple_name,),
69
+ )
70
+
71
+ parameters: list[Parameter] = []
72
+ for row in cursor.fetchall():
73
+ raw_name: str = row[0] # e.g. "@OrderId"
74
+ sql_type: str = row[1]
75
+ direction: str = row[2]
76
+ has_default: bool = row[3] or False
77
+ nullable: bool = row[4] or False
78
+
79
+ parameters.append(
80
+ Parameter(
81
+ name=raw_name,
82
+ sql_type=sql_type.upper(),
83
+ direction=direction, # type: ignore[arg-type]
84
+ default=None if not has_default else "(default)",
85
+ nullable=nullable,
86
+ )
87
+ )
88
+
89
+ # Fetch all result sets via sp_describe_first_result_set
90
+ result_sets: list[list[ColumnInfo]] = []
91
+ try:
92
+ tsql = f"EXEC {sp_name}"
93
+ for idx in range(10):
94
+ cursor.execute(
95
+ "{CALL sp_describe_first_result_set(?, NULL, ?)}",
96
+ (tsql, idx),
97
+ )
98
+ rows = cursor.fetchall()
99
+ columns = [
100
+ ColumnInfo(
101
+ name=row[2], # name (index 2)
102
+ sql_type=(row[5] or "UNKNOWN").upper(), # system_type_name (index 5)
103
+ nullable=bool(row[3]), # is_nullable (index 3)
104
+ )
105
+ for row in rows
106
+ if row[2] is not None # skip unnamed columns
107
+ ]
108
+ if not columns:
109
+ break
110
+ result_sets.append(columns)
111
+ except Exception:
112
+ # If sp_describe_first_result_set fails (e.g. dynamic SQL),
113
+ # return empty result sets rather than crashing.
114
+ pass
115
+
116
+ # Enrich with column descriptions from extended properties
117
+ for rs in result_sets:
118
+ descriptions = self._fetch_column_descriptions(sp_name, cursor, rs)
119
+ for col in rs:
120
+ col.description = descriptions.get(col.name)
121
+
122
+ return SPMetadata(name=sp_name, parameters=parameters, result_sets=result_sets)
@@ -0,0 +1,220 @@
1
+ Metadata-Version: 2.4
2
+ Name: singularitysql
3
+ Version: 0.1.0
4
+ Summary: SQL Server stored procedure introspection and Pydantic model generator
5
+ Author: Samuel Urrego
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: pyodbc>=5.0
11
+ Requires-Dist: pydantic>=2.0
12
+ Requires-Dist: typer>=0.9
13
+ Requires-Dist: tomli>=2.0
14
+ Requires-Dist: rich>=15.0.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8.0; extra == "dev"
17
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
18
+ Requires-Dist: pytest-mock>=3.14; extra == "dev"
19
+ Requires-Dist: ruff>=0.4; extra == "dev"
20
+ Requires-Dist: mypy>=1.10; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ <p align="center">
24
+ <img src="assets/logo.png" alt="Singularity" width="400">
25
+ </p>
26
+
27
+ > SQL Server → Pydantic v2. Automatically.
28
+
29
+ [![CI](https://github.com/Samuel-Urrego/Singularity/actions/workflows/ci.yml/badge.svg)](https://github.com/Samuel-Urrego/Singularity/actions/workflows/ci.yml)
30
+ [![Python](https://img.shields.io/pypi/pyversions/singularity.svg)](https://pypi.org/project/singularity/)
31
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
32
+
33
+ Singularity bridges the gap between SQL Server and Python. Connect to your database, point at a stored procedure, and get a **Pydantic v2 model** — as a runtime class or a `.py` file you can commit.
34
+
35
+ ```bash
36
+ singularity --config config.toml
37
+ ```
38
+
39
+ ```python
40
+ from singularity import SQLServerIntrospector, generate_model
41
+
42
+ introspector = SQLServerIntrospector("DRIVER={ODBC Driver 17};SERVER=...;DATABASE=...")
43
+ meta = introspector.introspect("usp_GetOrders")
44
+ model = generate_model(meta, mode="dynamic")
45
+ # <class 'pydantic.main.UspGetOrders'>
46
+ ```
47
+
48
+ ## Why Singularity?
49
+
50
+ **The problem**: You have dozens of complex stored procedures in SQL Server. Calling them from Python means manually writing Pydantic models for every parameter and result set. One typo, and you get a runtime error.
51
+
52
+ **The solution**: Singularity reads SQL Server's system catalog (`sys.parameters`, `sp_describe_first_result_set`) and generates the models for you. Zero manual mapping.
53
+
54
+ | Approach | Lines of code | Maintainable | Type-safe |
55
+ |---|---|---|---|
56
+ | Manual Pydantic models | 100s–1000s | ❌ | ⚠️ (manual) |
57
+ | Raw dicts / tuples | Fewer | ❌ | ❌ |
58
+ | **Singularity** | **Zero** | ✅ | ✅ |
59
+
60
+ ## Features
61
+
62
+ - 🔌 **Auto-connect** — pyodbc connection with `@@VERSION` detection
63
+ - 🧠 **Version-aware** — Modern (2016+), Legacy (2008–2014), and Azure SQL strategies
64
+ - 📦 **Two output modes**:
65
+ - `"dynamic"` — `create_model()` at runtime, usable immediately
66
+ - `"source"` — `.py` files you can commit and review
67
+ - 🏷️ **Full type mapping** — `INT`→`int`, `VARCHAR`→`str`, `DATETIME`→`datetime`, `BIT`→`bool`, etc.
68
+ - 🎨 **Configurable naming** — snake_case, camelCase, PascalCase for field names
69
+ - 🗂️ **File naming templates** — `{schema}_{sp_name}.py`, `{database}_{sp_name}.py`, etc.
70
+ - 🛡️ **Nullable awareness** — `Optional[T]` for nullable columns
71
+ - ⚡ **UV-first** — fast dependency management
72
+
73
+ ## Installation
74
+
75
+ ```bash
76
+ uv add singularity
77
+ # or
78
+ pip install singularity
79
+ ```
80
+
81
+ **Prerequisite**: [ODBC Driver for SQL Server](https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server) (17 or 18).
82
+
83
+ ## Quick Start
84
+
85
+ ### 1. Create a config file
86
+
87
+ ```toml
88
+ # config.toml
89
+ [connection]
90
+ server = "localhost"
91
+ database = "AdventureWorks"
92
+ driver = "ODBC Driver 18 for SQL Server"
93
+ trusted_connection = true
94
+
95
+ [sp_selection]
96
+ pattern = "usp_%"
97
+
98
+ [output]
99
+ directory = "generated_models"
100
+ mode = "source"
101
+ file_naming = "{schema}_{sp_name}.py"
102
+ naming_convention = "snake_case"
103
+ ```
104
+
105
+ ### 2. Generate models
106
+
107
+ ```bash
108
+ singularity --config config.toml
109
+ ```
110
+
111
+ ```
112
+ Connected. Detected version: modern
113
+ Introspecting usp_GetOrders... → generated_models/dbo_usp_GetOrders.py
114
+ Introspecting usp_GetCustomers... → generated_models/dbo_usp_GetCustomers.py
115
+
116
+ Done. 2 succeeded, 0 failed.
117
+ ```
118
+
119
+ ### 3. Use the generated models
120
+
121
+ ```python
122
+ from generated_models.dbo_usp_GetOrders import UspGetOrders
123
+
124
+ order = UspGetOrders(id=1, customer_name="Acme Corp", total=99.99)
125
+ ```
126
+
127
+ ## Library Usage
128
+
129
+ ```python
130
+ from singularity import SQLServerIntrospector, generate_model
131
+
132
+ # Connect and introspect
133
+ introspector = SQLServerIntrospector(conn_str)
134
+ introspector.connect()
135
+ version = introspector.detect_version() # ServerVersion.MODERN
136
+ metadata = introspector.introspect("usp_GetOrders")
137
+
138
+ # Runtime model
139
+ DynamicModel = generate_model(metadata, mode="dynamic")
140
+ instance = DynamicModel(id=1, customer_name="Acme")
141
+
142
+ # Source code string
143
+ source_code = generate_model(metadata, mode="source")
144
+ with open("models/usp_GetOrders.py", "w") as f:
145
+ f.write(source_code)
146
+ ```
147
+
148
+ ## Configuration Reference
149
+
150
+ ### `[connection]`
151
+
152
+ | Field | Required | Default | Description |
153
+ |---|---|---|---|
154
+ | `server` | ✅ | — | Server hostname or IP |
155
+ | `database` | ✅ | — | Database name |
156
+ | `driver` | ❌ | `ODBC Driver 18 for SQL Server` | ODBC driver name |
157
+ | `trusted_connection` | ❌ | `true` | Use Windows auth |
158
+ | `username` | ❌ | — | SQL auth username |
159
+ | `password` | ❌ | — | SQL auth password |
160
+
161
+ ### `[sp_selection]`
162
+
163
+ | Field | Required | Description |
164
+ |---|---|---|
165
+ | `procedures` | ❌ | Explicit list of SP names |
166
+ | `pattern` | ❌ | Wildcard pattern (e.g. `usp_%`) |
167
+
168
+ At least one of `procedures` or `pattern` must be specified.
169
+
170
+ ### `[output]`
171
+
172
+ | Field | Required | Default | Description |
173
+ |---|---|---|---|
174
+ | `directory` | ❌ | `.` | Output directory |
175
+ | `mode` | ❌ | `source` | `source` or `dynamic` |
176
+ | `file_naming` | ❌ | `{sp_name}.py` | Template with `{schema}`, `{database}`, `{sp_name}` |
177
+ | `naming_convention` | ❌ | `snake_case` | `snake_case`, `camelCase`, or `PascalCase` |
178
+
179
+ ## Supported SQL Server Versions
180
+
181
+ | Version | Strategy | Parameter introspection | Result set metadata |
182
+ |---|---|---|---|
183
+ | 2016+ | Modern | `sys.parameters` | `sp_describe_first_result_set` |
184
+ | 2008–2014 | Legacy | `sys.parameters` | `sp_describe_first_result_set` + `sys.columns` fallback |
185
+ | Azure SQL | Azure | `sys.parameters` | `sys.dm_exec_describe_first_result_set` |
186
+
187
+ ## Type Mapping
188
+
189
+ | SQL Server | Python | Pydantic |
190
+ |---|---|---|
191
+ | `INT`, `BIGINT`, `SMALLINT`, `TINYINT` | `int` | `int` |
192
+ | `VARCHAR`, `NVARCHAR`, `CHAR`, `NCHAR`, `TEXT` | `str` | `str` |
193
+ | `DATETIME`, `DATETIME2`, `DATE`, `SMALLDATETIME` | `datetime` | `datetime` |
194
+ | `BIT` | `bool` | `bool` |
195
+ | `DECIMAL`, `NUMERIC`, `FLOAT`, `REAL`, `MONEY` | `float` | `float` |
196
+ | `UNIQUEIDENTIFIER` | `str` | `str` |
197
+ | Unknown types | `str` + warning | `str` |
198
+
199
+ ## Development
200
+
201
+ ```bash
202
+ # Clone and install
203
+ git clone https://github.com/Samuel-Urrego/Singularity
204
+ cd singularity
205
+ uv sync
206
+
207
+ # Run tests
208
+ uv run pytest
209
+
210
+ # Lint and type-check
211
+ uv run ruff check .
212
+ uv run mypy singularity/
213
+
214
+ # Install pre-commit hooks
215
+ uv run pre-commit install
216
+ ```
217
+
218
+ ## License
219
+
220
+ MIT — see [LICENSE](LICENSE) for details.