# Native SQL Operations Always default to Native GraphQL. Use Native SQL **only** when you need database-specific features not available in GraphQL (e.g., PostGIS, Window Functions, Complex Aggregations, or specific DML CTEs). ## Core Agent Constraints When generating Native SQL operations, you are bypassing GraphQL and talking directly to PostgreSQL. You **MUST** adhere to these strict constraints: 1. **Operation Syntax Isolation:** Never mix Native SQL positional parameters (`$1`) with standard GraphQL named variables (`$id`). The `sql:` argument MUST be a hardcoded string literal block (`"""SELECT..."""`), not a GraphQL variable. 1. **Table & Column Mapping (Case Sensitivity):** - **Default `snake_case` Conversion:** By default, SQL Connect converts `PascalCase` types and `camelCase` fields to `snake_case` in the database. - *Schema:* `type UserProfile { releaseYear: Int }` -> *Native SQL:* `SELECT release_year FROM user_profile` - **Explicit Overrides (Requires Double Quotes):** If the schema uses `@table(name: "ExactName")` or `@col(name: "ExactCol")`, you **MUST wrap the identifier in double quotes** if it contains capital letters (e.g., `SELECT * FROM "ExactName"`). Without quotes, Postgres folds it to lowercase and fails validation. ## Syntax rules & limitations Native SQL enforces strict parsing rules to ensure security and prevent SQL injection: - **String Literals Only:** The `sql` argument must be a hardcoded string literal block (`"""SELECT..."""`) directly in the `.gql` file. It **cannot** be a GraphQL variable. - **Validation:** Do **NOT** use DDL in any operations (modify the `schema.gql` file instead for table/column changes). Furthermore, `query` operations cannot contain DML and must start with `SELECT`, `TABLE`, or `WITH`. - **Parameters:** Use strict positional parameters (`$1`, `$2`) that match the `params` array order. Named parameters (`$id`, `:name`) are **forbidden**. - **Comments:** Use block comments (`/* ... */`). Line comments (`--`) are **forbidden** because they can truncate subsequent clauses during query compilation. If you comment out a line containing a parameter (e.g., `/* WHERE id = $1 */`), you must also remove that parameter from the `params` list, or it will fail with `unused parameter: $1`. - **Strings:** Extended string literals (`E'...'`) and dollar-quoted strings (`$$...$$`) are supported. - **Context Maps (`_expr`):** Variables **cannot** be used inside `_expr` fields; to ensure security, `_expr` must be a static string (e.g., `{_expr: "auth.uid"}`, not `{_expr: $uidVar}`). ## Native SQL Root Fields Operations are executed using the permissions granted to the SQL Connect service account. You can alias the root field (e.g., `movies: _select`) to make the client response cleaner (`data.movies` instead of `data._select`). > **Note on `Any` Return Types:** Because Native SQL completely bypasses > GraphQL's strong typing, queries like `_select` and `_executeReturning` return > the generic `Any` scalar type. The generated client SDKs (TypeScript, Swift, > Kotlin, Dart) will type this as `any` (or equivalent). **AGENT INSTRUCTION**: > When you generate client-side code that consumes these operations, you MUST > manually cast or validate the shape of the data, as the typical type safety of > SQL Connect will not be present. Use these root fields in `query` or `mutation` operations: ### Query Fields (Read-Only) - `_select`: Executes a SQL query returning zero or more rows. Returns `[Any]`. ```graphql query GetMovies($genre: String!) @auth(level: PUBLIC) { movies: _select( sql: "SELECT id, title FROM movie WHERE genre = $1", params: [$genre] ) } ``` - `_selectFirst`: Executes a SQL query expected to return zero or one row. Returns `Any` or `null`. ```graphql query GetTotalReviews @auth(level: PUBLIC) { stats: _selectFirst( sql: "SELECT COUNT(*) as total_reviews FROM review" ) # params can be omitted if empty } ``` ### Mutation Fields (DML) - `_execute`: Executes DML (`INSERT`, `UPDATE`, `DELETE`). Returns `Int` (number of rows affected). - *Note 1:* `RETURNING` clauses are ignored in the result. - *Note 2:* Only `_execute` supports Data-Modifying Common Table Expressions (e.g., `WITH new_row AS (INSERT...)`). ```graphql mutation UpdateRating($id: UUID!, $rating: Float!) @auth(level: USER) { _execute( sql: "UPDATE movie SET rating = $2 WHERE id = $1", params: [$id, $rating] ) } ``` - `_executeReturning`: Executes DML with a `RETURNING` clause. Returns `[Any]`. Data-Modifying CTEs are **not** supported. ```graphql mutation DeleteUserReviews($uid: String!) @auth(level: USER) { deletedReviews: _executeReturning( sql: "DELETE FROM review WHERE user_id = $1 RETURNING id, rating", params: [{_expr: "auth.uid"}] ) } ``` - `_executeReturningFirst`: Executes DML with `RETURNING`, expecting zero or one row. Returns `Any` or `null`. Data-Modifying CTEs are **not** supported. ```graphql mutation UpdateMyReview($movieId: UUID!, $text: String!) @auth(level: USER) { updatedReview: _executeReturningFirst( sql: """ UPDATE review SET text = $2 WHERE movie_id = $1 AND user_id = $3 RETURNING id, text """, params: [$movieId, $text, {_expr: "auth.uid"}] ) } ``` ### PostgreSQL Extensions Native SQL allows you to directly query and utilize PostgreSQL extensions, such as `PostGIS`, without needing to map complex geometry types into your GraphQL schema or alter your underlying tables (e.g., using JSON operators to extract values and pass them into `ST_MakePoint`). *Note: You must enable the extension on your underlying Cloud SQL instance by connecting as the `postgres` user and running `CREATE EXTENSION IF NOT EXISTS ...;`* *(See `examples.md` for a full `GetNearbyActiveRestaurants` implementation).* ## ⚠️ Security: Stored Procedures & Dynamic SQL SQL Connect parameterizes inputs at the GraphQL boundary automatically. However, if your Native SQL calls **custom PL/pgSQL stored procedures**, you must manually prevent 2nd-order SQL injection: - **NEVER** concatenate user input into an `EXECUTE` string (`EXECUTE 'UPDATE ' || table || ' SET x=' || val;`). - **DO** use the `USING` clause to bind data values safely. - **DO** use `format('%I')` for safe database identifier injection. - **DO** validate dynamic table/column names against a strict hardcoded allowlist. **Secure PL/pgSQL Pattern:** ```sql CREATE OR REPLACE PROCEDURE secure_update(target_table TEXT, new_value TEXT, row_id INT) LANGUAGE plpgsql AS $$ BEGIN -- 1. Strict Allowlist for Identifiers IF target_table NOT IN ('orders', 'users', 'inventory') THEN RAISE EXCEPTION 'Invalid table name'; END IF; -- 2. format(%I) for Identifiers, USING for Data EXECUTE format('UPDATE %I SET status = $1 WHERE id = $2', target_table) USING new_value, row_id; END; $$; ```