Skip to content

Commit 446d60a

Browse files
authored
feat: add support for Google Cloud SQL and AlloyDB connectors (#229)
Introduce integration for Google Cloud SQL and AlloyDB connectors, enabling simplified authentication and connection management with automatic support for the `asyncpg` driver.
1 parent 5a33229 commit 446d60a

File tree

14 files changed

+1975
-27
lines changed

14 files changed

+1975
-27
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
uses: astral-sh/setup-uv@v3
1717

1818
- name: Set up Python
19-
run: uv python install 3.10
19+
run: uv python install 3.11
2020

2121
- name: Create virtual environment
2222
run: uv sync --all-extras --dev
@@ -43,7 +43,7 @@ jobs:
4343
uses: astral-sh/setup-uv@v3
4444

4545
- name: Set up Python
46-
run: uv python install 3.10
46+
run: uv python install 3.11
4747

4848
- name: Install dependencies
4949
run: uv sync --all-extras --dev
@@ -60,7 +60,7 @@ jobs:
6060
uses: astral-sh/setup-uv@v3
6161

6262
- name: Set up Python
63-
run: uv python install 3.10
63+
run: uv python install 3.11
6464

6565
- name: Install dependencies
6666
run: uv sync --all-extras --dev
@@ -77,7 +77,7 @@ jobs:
7777
uses: astral-sh/setup-uv@v3
7878

7979
- name: Set up Python
80-
run: uv python install 3.10
80+
run: uv python install 3.11
8181

8282
- name: Install dependencies
8383
run: uv sync --all-extras --dev

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ jobs:
105105
CIBW_ENVIRONMENT: "HATCH_BUILD_HOOKS_ENABLE=1 MYPYC_OPT_LEVEL=3 MYPYC_DEBUG_LEVEL=0 MYPYC_MULTI_FILE=1"
106106

107107
# Test the built wheels
108+
CIBW_TEST_REQUIRES: "cloud-sql-python-connector google-cloud-alloydb-connector"
108109
CIBW_TEST_COMMAND: "python -c \"import sqlspec; print('MyPyC wheel test passed')\""
109110

110111
- name: Upload wheel artifacts

.github/workflows/test-build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ jobs:
117117
CIBW_ENVIRONMENT: "HATCH_BUILD_HOOKS_ENABLE=1 MYPYC_OPT_LEVEL=3 MYPYC_DEBUG_LEVEL=0 MYPYC_MULTI_FILE=1"
118118

119119
# Test the built wheels
120+
CIBW_TEST_REQUIRES: "cloud-sql-python-connector google-cloud-alloydb-connector"
120121
CIBW_TEST_COMMAND: "python -c \"import sqlspec; print('MyPyC wheel test passed')\""
121122

122123
- name: Upload wheel artifacts

AGENTS.md

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,321 @@ Current state of all adapters (as of type-cleanup branch):
19121912
- **Excellent**: Follows all patterns, well documented
19131913
- **Good**: Follows patterns appropriately for adapter's needs
19141914

1915+
## Google Cloud Connector Pattern
1916+
1917+
### Overview
1918+
1919+
Google Cloud SQL and AlloyDB connectors provide automatic IAM authentication, SSL management, and IP routing for PostgreSQL databases hosted on Google Cloud Platform. SQLSpec integrates these connectors through the AsyncPG adapter using a connection factory pattern.
1920+
1921+
### When to Use This Pattern
1922+
1923+
Use Google Cloud connectors when:
1924+
1925+
- Connecting to Cloud SQL for PostgreSQL instances
1926+
- Connecting to AlloyDB for PostgreSQL clusters
1927+
- Need automatic IAM authentication
1928+
- Want managed SSL/TLS connections
1929+
- Require private IP or PSC connectivity
1930+
1931+
### Implementation Pattern
1932+
1933+
#### Step 1: Add Optional Dependencies
1934+
1935+
Add connector packages as optional dependency groups in pyproject.toml:
1936+
1937+
```toml
1938+
[project.optional-dependencies]
1939+
cloud-sql = ["cloud-sql-python-connector[asyncpg]"]
1940+
alloydb = ["cloud-alloydb-python-connector[asyncpg]"]
1941+
```
1942+
1943+
#### Step 2: Add Detection Constants
1944+
1945+
In sqlspec/_typing.py:
1946+
1947+
```python
1948+
try:
1949+
import google.cloud.sql.connector
1950+
CLOUD_SQL_CONNECTOR_INSTALLED = True
1951+
except ImportError:
1952+
CLOUD_SQL_CONNECTOR_INSTALLED = False
1953+
1954+
try:
1955+
import google.cloud.alloydb.connector
1956+
ALLOYDB_CONNECTOR_INSTALLED = True
1957+
except ImportError:
1958+
ALLOYDB_CONNECTOR_INSTALLED = False
1959+
```
1960+
1961+
Re-export in sqlspec/typing.py and add to **all**.
1962+
1963+
#### Step 3: Update Driver Features TypedDict
1964+
1965+
Document all connector options with comprehensive descriptions:
1966+
1967+
```python
1968+
class AsyncpgDriverFeatures(TypedDict):
1969+
"""AsyncPG driver feature flags."""
1970+
1971+
enable_cloud_sql: NotRequired[bool]
1972+
"""Enable Google Cloud SQL connector integration.
1973+
Requires cloud-sql-python-connector package.
1974+
Defaults to True when package is installed.
1975+
Auto-configures IAM authentication, SSL, and IP routing.
1976+
Mutually exclusive with enable_alloydb.
1977+
"""
1978+
1979+
cloud_sql_instance: NotRequired[str]
1980+
"""Cloud SQL instance connection name.
1981+
Format: "project:region:instance"
1982+
Required when enable_cloud_sql is True.
1983+
"""
1984+
1985+
cloud_sql_enable_iam_auth: NotRequired[bool]
1986+
"""Enable IAM database authentication.
1987+
Defaults to False for passwordless authentication.
1988+
When False, requires user/password in pool_config.
1989+
"""
1990+
1991+
cloud_sql_ip_type: NotRequired[str]
1992+
"""IP address type for connection.
1993+
Options: "PUBLIC", "PRIVATE", "PSC"
1994+
Defaults to "PRIVATE".
1995+
"""
1996+
1997+
enable_alloydb: NotRequired[bool]
1998+
"""Enable Google AlloyDB connector integration.
1999+
Requires cloud-alloydb-python-connector package.
2000+
Defaults to True when package is installed.
2001+
Auto-configures IAM authentication and private networking.
2002+
Mutually exclusive with enable_cloud_sql.
2003+
"""
2004+
2005+
alloydb_instance_uri: NotRequired[str]
2006+
"""AlloyDB instance URI.
2007+
Format: "projects/PROJECT/locations/REGION/clusters/CLUSTER/instances/INSTANCE"
2008+
Required when enable_alloydb is True.
2009+
"""
2010+
2011+
alloydb_enable_iam_auth: NotRequired[bool]
2012+
"""Enable IAM database authentication.
2013+
Defaults to False for passwordless authentication.
2014+
"""
2015+
2016+
alloydb_ip_type: NotRequired[str]
2017+
"""IP address type for connection.
2018+
Options: "PUBLIC", "PRIVATE", "PSC"
2019+
Defaults to "PRIVATE".
2020+
"""
2021+
```
2022+
2023+
#### Step 4: Add Auto-Detection to Config Init
2024+
2025+
```python
2026+
class AsyncpgConfig(AsyncDatabaseConfig):
2027+
def __init__(self, *, driver_features=None, **kwargs):
2028+
features_dict = dict(driver_features) if driver_features else {}
2029+
2030+
features_dict.setdefault("enable_cloud_sql", CLOUD_SQL_CONNECTOR_INSTALLED)
2031+
features_dict.setdefault("enable_alloydb", ALLOYDB_CONNECTOR_INSTALLED)
2032+
2033+
super().__init__(driver_features=features_dict, **kwargs)
2034+
2035+
self._cloud_sql_connector = None
2036+
self._alloydb_connector = None
2037+
2038+
self._validate_connector_config()
2039+
```
2040+
2041+
#### Step 5: Add Configuration Validation
2042+
2043+
```python
2044+
def _validate_connector_config(self) -> None:
2045+
"""Validate Google Cloud connector configuration."""
2046+
enable_cloud_sql = self.driver_features.get("enable_cloud_sql", False)
2047+
enable_alloydb = self.driver_features.get("enable_alloydb", False)
2048+
2049+
if enable_cloud_sql and enable_alloydb:
2050+
msg = "Cannot enable both Cloud SQL and AlloyDB connectors simultaneously. Use separate configs for each database."
2051+
raise ImproperConfigurationError(msg)
2052+
2053+
if enable_cloud_sql:
2054+
if not CLOUD_SQL_CONNECTOR_INSTALLED:
2055+
msg = "cloud-sql-python-connector package not installed. Install with: pip install cloud-sql-python-connector"
2056+
raise ImproperConfigurationError(msg)
2057+
2058+
instance = self.driver_features.get("cloud_sql_instance")
2059+
if not instance:
2060+
msg = "cloud_sql_instance required when enable_cloud_sql is True. Format: 'project:region:instance'"
2061+
raise ImproperConfigurationError(msg)
2062+
2063+
cloud_sql_instance_parts_expected = 2
2064+
if instance.count(":") != cloud_sql_instance_parts_expected:
2065+
msg = f"Invalid Cloud SQL instance format: {instance}. Expected format: 'project:region:instance'"
2066+
raise ImproperConfigurationError(msg)
2067+
2068+
elif enable_alloydb:
2069+
if not ALLOYDB_CONNECTOR_INSTALLED:
2070+
msg = "cloud-alloydb-python-connector package not installed. Install with: pip install cloud-alloydb-python-connector"
2071+
raise ImproperConfigurationError(msg)
2072+
2073+
instance_uri = self.driver_features.get("alloydb_instance_uri")
2074+
if not instance_uri:
2075+
msg = "alloydb_instance_uri required when enable_alloydb is True. Format: 'projects/PROJECT/locations/REGION/clusters/CLUSTER/instances/INSTANCE'"
2076+
raise ImproperConfigurationError(msg)
2077+
2078+
if not instance_uri.startswith("projects/"):
2079+
msg = f"Invalid AlloyDB instance URI format: {instance_uri}. Expected format: 'projects/PROJECT/locations/REGION/clusters/CLUSTER/instances/INSTANCE'"
2080+
raise ImproperConfigurationError(msg)
2081+
```
2082+
2083+
#### Step 6: Implement Connection Factory Pattern
2084+
2085+
Extract connector setup into private helper methods:
2086+
2087+
```python
2088+
def _setup_cloud_sql_connector(self, config: dict[str, Any]) -> None:
2089+
"""Setup Cloud SQL connector and configure pool for connection factory pattern."""
2090+
from google.cloud.sql.connector import Connector
2091+
2092+
self._cloud_sql_connector = Connector()
2093+
2094+
user = config.get("user")
2095+
password = config.get("password")
2096+
database = config.get("database")
2097+
2098+
async def get_conn() -> AsyncpgConnection:
2099+
conn_kwargs = {
2100+
"instance_connection_string": self.driver_features["cloud_sql_instance"],
2101+
"driver": "asyncpg",
2102+
"enable_iam_auth": self.driver_features.get("cloud_sql_enable_iam_auth", False),
2103+
"ip_type": self.driver_features.get("cloud_sql_ip_type", "PRIVATE"),
2104+
}
2105+
2106+
if user:
2107+
conn_kwargs["user"] = user
2108+
if password:
2109+
conn_kwargs["password"] = password
2110+
if database:
2111+
conn_kwargs["db"] = database
2112+
2113+
return await self._cloud_sql_connector.connect_async(**conn_kwargs)
2114+
2115+
for key in ("dsn", "host", "port", "user", "password", "database"):
2116+
config.pop(key, None)
2117+
2118+
config["connect"] = get_conn
2119+
2120+
2121+
def _setup_alloydb_connector(self, config: dict[str, Any]) -> None:
2122+
"""Setup AlloyDB connector and configure pool for connection factory pattern."""
2123+
from google.cloud.alloydb.connector import AsyncConnector
2124+
2125+
self._alloydb_connector = AsyncConnector()
2126+
2127+
user = config.get("user")
2128+
password = config.get("password")
2129+
database = config.get("database")
2130+
2131+
async def get_conn() -> AsyncpgConnection:
2132+
conn_kwargs = {
2133+
"instance_uri": self.driver_features["alloydb_instance_uri"],
2134+
"driver": "asyncpg",
2135+
"enable_iam_auth": self.driver_features.get("alloydb_enable_iam_auth", False),
2136+
"ip_type": self.driver_features.get("alloydb_ip_type", "PRIVATE"),
2137+
}
2138+
2139+
if user:
2140+
conn_kwargs["user"] = user
2141+
if password:
2142+
conn_kwargs["password"] = password
2143+
if database:
2144+
conn_kwargs["db"] = database
2145+
2146+
return await self._alloydb_connector.connect(**conn_kwargs)
2147+
2148+
for key in ("dsn", "host", "port", "user", "password", "database"):
2149+
config.pop(key, None)
2150+
2151+
config["connect"] = get_conn
2152+
```
2153+
2154+
#### Step 7: Use in Pool Creation
2155+
2156+
```python
2157+
async def _create_pool(self) -> Pool[Record]:
2158+
config = self._get_pool_config_dict()
2159+
2160+
if self.driver_features.get("enable_cloud_sql", False):
2161+
self._setup_cloud_sql_connector(config)
2162+
elif self.driver_features.get("enable_alloydb", False):
2163+
self._setup_alloydb_connector(config)
2164+
2165+
if "init" not in config:
2166+
config["init"] = self._init_connection
2167+
2168+
return await asyncpg_create_pool(**config)
2169+
```
2170+
2171+
#### Step 8: Cleanup Connectors
2172+
2173+
```python
2174+
async def _close_pool(self) -> None:
2175+
if self.pool_instance:
2176+
await self.pool_instance.close()
2177+
2178+
if self._cloud_sql_connector is not None:
2179+
await self._cloud_sql_connector.close_async()
2180+
self._cloud_sql_connector = None
2181+
2182+
if self._alloydb_connector is not None:
2183+
await self._alloydb_connector.close()
2184+
self._alloydb_connector = None
2185+
```
2186+
2187+
### Key Design Principles
2188+
2189+
1. **Auto-Detection**: Default to package installation status
2190+
2. **Mutual Exclusion**: Cannot enable both connectors simultaneously
2191+
3. **Connection Factory Pattern**: Use driver's `connect` parameter
2192+
4. **Clean Helper Methods**: Extract setup logic for maintainability
2193+
5. **Proper Lifecycle**: Initialize in create_pool, cleanup in close_pool
2194+
6. **Clear Validation**: Validate instance names, package installation, config
2195+
7. **Comprehensive TypedDict**: Document all options inline
2196+
2197+
### Testing Requirements
2198+
2199+
- Unit tests with mocked connectors
2200+
- Integration tests with real instances (conditional)
2201+
- Test auto-detection with both packages installed/not installed
2202+
- Test mutual exclusion validation
2203+
- Test connection factory pattern integration
2204+
- Test lifecycle (initialization and cleanup)
2205+
- Test all IP types and auth modes
2206+
2207+
### Driver Compatibility
2208+
2209+
| Driver | Cloud SQL | AlloyDB | Notes |
2210+
|--------|-----------|---------|-------|
2211+
| AsyncPG | ✅ Full | ✅ Full | Connection factory pattern via `connect` param |
2212+
| Psycopg | ⚠️ Research | ⚠️ Research | Not officially documented, needs prototype |
2213+
| Psqlpy | ❌ No | ❌ No | Internal Rust driver, architecturally incompatible |
2214+
| ADBC | ❌ No | ❌ No | URI-only interface, no factory pattern support |
2215+
2216+
### Examples from Existing Implementations
2217+
2218+
See sqlspec/adapters/asyncpg/config.py for the reference implementation.
2219+
2220+
### Documentation Requirements
2221+
2222+
When implementing cloud connector support:
2223+
2224+
1. **Update adapter guide** - Add cloud integration section with examples
2225+
2. **Create cloud connector guide** - Comprehensive configuration reference
2226+
3. **Document limitations** - Clearly state unsupported drivers
2227+
4. **Provide troubleshooting** - Common errors and solutions
2228+
5. **Include migration guide** - From direct DSN to connector pattern
2229+
19152230
### Testing Requirements
19162231

19172232
When implementing `driver_features`, you MUST test:

0 commit comments

Comments
 (0)