> ## Documentation Index
> Fetch the complete documentation index at: https://hoopdev-docs-control-plane-owns-listeners.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# MySQL

> What the MySQL codec decodes, what it refuses, and how a mysql lane masks

A `mysql` lane decodes the MySQL client/server protocol: the SQL a client sends over `COM_QUERY`, the statements behind every prepared-statement id, and the result sets the server returns in both the text and the binary encoding.

```yaml config.yaml theme={null}
listeners:
  - name: appdb
    protocol: mysql
    listen: 127.0.0.1:13306
    upstream: appdb:3306
    guardrails:
      rules:
        - name: no-destructive-sql
          type: operation
          operations: [drop, delete, truncate]
          message: destructive statements are not permitted on appdb
    mask:
      rules:
        - {name: email-column, columns: [email], strategy: redact}
```

***

## What the codec reads

A MySQL byte means different things depending on what the handshake negotiated and on the command in flight, where a pgwire message declares its own type and length. The codec tracks both, one instance per connection, across both directions:

* **Capabilities.** The codec latches the negotiated set from the client's handshake response. `CLIENT_DEPRECATE_EOF` alone decides whether a result set ends with an EOF packet or an OK packet that begins with the same byte.
* **Prepared statements.** `COM_STMT_EXECUTE` carries no SQL, only the numeric id the server assigned in its reply to `COM_STMT_PREPARE`. The codec keeps that map and attributes each execute to its text, which is the path ORMs take.
* **Multi-statements.** Connector/J and most ORMs negotiate `CLIENT_MULTI_STATEMENTS` by default, so `SELECT 1; DROP TABLE users` arrives as one `COM_QUERY`. The codec splits it with MySQL's own lexical rules (backtick identifiers, `#` comments, backslash escapes) before classifying, so a statement hidden after a string literal cannot ride through under a `select` classification.

***

## What it refuses

Three negotiated features would make later bytes unreadable, and the codec refuses the stream instead of forwarding what it cannot inspect. Each refusal closes the connection with an operator-facing reason:

| Refusal                         | Why                                                                                     | Client-side fix                                    |
| ------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------- |
| Compression (`CLIENT_COMPRESS`) | Replaces the packet framing itself from the first byte after the handshake              | Do not enable `--compress`                         |
| TLS upgrade (`CLIENT_SSL`)      | Every later byte is a TLS record the lane is not terminating                            | `--ssl-mode=DISABLED`, `useSSL=false`, `tls=false` |
| `LOAD DATA LOCAL INFILE`        | Opens a data channel with no statements, and lets a server read any path the client can | `--local-infile=0`, `allowAllFiles=false`          |

A codec that forwards what it cannot parse turns into a bypass: the session works, no policy runs, the audit trail records nothing, and the gap stays invisible until an audit finds it. A refusal surfaces one clear error at the moment of the negotiation.

***

## TLS on each leg

MySQL negotiates TLS in-band, and unlike pgwire the server greets **first**: the client's upgrade request is a truncated handshake response rather than a self-describing packet. The lane terminates neither side of that exchange:

| Leg             | Options                                                                                                     |
| --------------- | ----------------------------------------------------------------------------------------------------------- |
| client → lane   | Plaintext only: connect with `--ssl-mode=DISABLED`. Startup refuses `downstream_tls` on a `mysql` lane.     |
| lane → database | Plaintext only: the relay speaks no in-band upgrade on the upstream hop either. Leave `upstream_tls` unset. |

Both hops carry the protocol in the clear by design, so keep them where that is acceptable: loopback, a unix socket, one pod, or a network a NetworkPolicy narrows. The [Transport](/setup/configuration/hoop-sidecar/config-file#transport) section covers the boundary each choice draws.

***

## Masking

The codec re-frames result sets in **both** encodings, because the two share nothing:

* **Text protocol** rows are length-encoded strings, and the codec rewrites them value by value.
* **Binary protocol** rows, the encoding prepared statements return, carry a NULL bitmap and type-driven values. The codec rewrites string-typed columns and leaves a numeric column alone rather than corrupting it.

A `NULL` survives masking as a `NULL`: re-encoding it as an empty string would turn "no value" into "the empty string" and change what the client computes. Column rules match the names the server declared in the result set's column definitions.

***

## Denials

A denied statement returns a native `ERR_Packet`, and the session stays usable afterwards:

```
ERROR 1142 (42000): destructive statements are not permitted on appdb
```

Dropping the socket instead would print "Lost connection to MySQL server during query": an outage message for a policy decision, which sends the developer to support instead of to their own query.

***

## The Envoy lane

Envoy's MySQL filter parses no SQL, so the lane is plain `tcp_proxy`, same shape as the [Postgres one](/setup/configuration/hoop-sidecar/protocols/postgres#the-envoy-lane) with the cluster pointed at the `mysql` listener's port:

```yaml envoy.yaml theme={null}
listeners:
  - name: mysql_ingress
    address:
      socket_address: { address: 0.0.0.0, port_value: 3306 }
    filter_chains:
      - filters:
          - name: envoy.filters.network.tcp_proxy
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
              stat_prefix: ingress_mysql
              cluster: hoop_inspect_mysql
              idle_timeout: 3600s   # a session idles between keystrokes
```

Verify the lane from a client:

```bash theme={null}
mysql -h envoy -P 3306 -u appuser -p --ssl-mode=DISABLED appdb \
  -e "SELECT id, name, email FROM customers"
```

***

## Next

<CardGroup cols={2}>
  <Card title="Config File Reference" icon="file-code" href="/setup/configuration/hoop-sidecar/config-file">
    Every listener field, inheritance between lanes, and what startup refuses.
  </Card>

  <Card title="Data Masking" icon="mask" href="/features/data-masking">
    Strategies, entity types, and the column-versus-detection tradeoff.
  </Card>
</CardGroup>
