spanner lane runs the same in-process HTTP/2 endpoint as a gRPC lane and adds one capability: it extracts the SQL text out of Cloud Spanner RPC payloads and classifies each statement with the lexer for that database’s dialect, GoogleSQL or PostgreSQL. A gRPC lane fences Spanner by method name. A spanner lane reads the query, so a rule naming delete refuses ExecuteSql when the SQL deletes and forwards it when the SQL selects.
config.yaml
Which RPCs yield SQL
The lane reads two services. Every other method on a Spanner endpoint (sessions, transactions, instance admin) keeps the gRPC lane’s per-message statement, where method fencing and payload rules still apply.
Each extracted string becomes its own statement: text, operation, effects, and each relation as a read or a write, the same document a postgres lane produces.
spanner.sql_index in the metadata names the member inside a batch, so the audit trail says which statement of an ExecuteBatchDml a denial refused. One denial refuses the whole message, because the batch commits atomically upstream.
Two rule families read one RPC, on different statements. The request-headers statement keeps the service and method in Tables, so a table rule fences a method before the upstream is dialed. The SQL statements carry the relations the query names, so operation and table rules about data read the query. A method fence never fires on query relations, and a data rule never fires on the RPC envelope.
Dialects: GoogleSQL and PostgreSQL
Spanner fixes a database’s dialect when you create it, and one instance holds databases of both kinds. The data plane never says which one a request targets:ExecuteSql carries a session name and a string. Read a PostgreSQL database with the GoogleSQL lexer and "songs" turns from a table into a string literal, so a table rule fencing songs stops firing. The lane therefore takes the dialect from configuration, keyed on the database resource name that every SQL-bearing request already carries, and never guesses it from the text. A client controls the text; a client that could pick the grammar could pick the one that hides its statement.
config.yaml
The lane reads the database from
session on the data-plane methods and from database on UpdateDatabaseDdl. CreateDatabase is the one request that states its own dialect, in database_dialect, and the lane believes it for that request’s statements: they create the database they name. Each extracted statement records spanner.dialect and spanner.database in its metadata, so the audit trail says which lexer read it and against what.
The PostgreSQL side uses the same lexer as a postgres lane, with "ident", $1 parameters and E'' strings, and the PostgreSQL conformance suite checks it. GoogleSQL has its own rule set, because each item below is a misread that flips a verdict:
"..."is a string literal. Postgres rules would read it as a quoted identifier and invent relations.- Raw strings (
r'...',rb'...') keep backslashes literal. A scanner that lets\'escape the terminator swallows the rest of the statement into a phantom literal. - Triple-quoted strings (
'''...''',"""...""") hold data. ADELETEinside one stays a select. - Backtick identifiers escape with a backslash, and
#opens a comment. - Statement hints and table hints (
@{FORCE_INDEX=idx}) skip cleanly, soFROM albums@{FORCE_INDEX=i}still reports the relationalbums.
unknown, and so does a SQL-bearing RPC whose payload the lane could not parse (a capture truncated at max_payload_bytes, for example). Both cases fail closed under a rule naming unknown. Without that classification, padding a request past the capture budget would smuggle any DML through the lane.
Descriptors
Extraction decodes payloads, so the lane requires a descriptor set. Google publishes the protos; onebuf invocation against the public tree produces an artifact covering Spanner and BigQuery Storage:
-grpc-discover answers Unimplemented against them and the buf route is the working one. An upstream that does serve reflection (your own services, most Go and Java servers with the reflection service registered) bootstraps its set with that command instead. Pin the artifact next to the config, or publish it to a bucket and point descriptors at gs://BUCKET/spanner.pb?generation=N; see Descriptor sources. The lane reads the artifact once, when its endpoint is built, and never the upstream’s reflection.
Client wiring
Against real GCP. grpc-go refuses to attach OAuth credentials to an insecure channel, so the client’s hop to the lane must be TLS: give the lanedownstream_tls with a certificate the client trusts. The lane rewrites :authority to the upstream host and forwards the authorization metadata untouched, so a standard endpoint override is the whole client change:
Limits
- Capturing lanes refuse compressed messages and strip
grpc-accept-encodingupstream. Google SDKs leave compression off by default; a client that enables gzip fails with code 12. - A capturing lane holds one message up to 16 MiB for inspection. Spanner allows commits far past that, so a workload with large mutations belongs on a method-only lane (drop
capture_payload) or behind a size review. - Response masking by field name has nothing to bind to: Spanner returns rows as positional
google.protobuf.Valuelists. Request-side fields mask; result columns keep their values.
Try it
Two compose stacks in the repository run the lane end to end:deploy/docker-compose/gcloud-stack/: the Spanner emulator behind a spanner lane, no Envoy. Its demo proves the lexer decides:SELECT 1passes,DELETEreturnsPERMISSION_DENIEDbefore the emulator sees the frame, a raw-string evasion stays a select, and an unterminated string denies asunknown.deploy/docker-compose/envoy-stack/spanner/: the same lane behind Envoy and OPA, plus a direct h2c door that shows the lane standing alone. Its demo also creates a second database withdatabaseDialect: POSTGRESQL, sendsSELECT id, title FROM "songs" ...to both, and reads the trail:postgresqlwithtables=[songs]on one,googlesqlwith no table on the other.
Next
gRPC
The transport this lane runs on: descriptors, strict mode, TLS shapes, Envoy wiring.
BigQuery
The Storage API on a grpc lane, and where the REST plane fits.