Repository navigation
feat: a Go client, written against the HTTP surface - #20
Merged
Merged
Conversation
`clients/go` was an empty directory. The Python client landed in #19 and the same argument applies here: anyone reaching for bigdb from Go had to write their own HTTP and their own escaping, and escaping is where a mistake is a security bug rather than a wrong answer. `archived/featurebase` is Go and is not a starting point - it speaks a different server's routes. Same scope as the Python client: `/sql`, `/table/{t}/query`, `/table/{t}/import`, `/table/{t}/delete`, `/table/{t}/records`, `/schema`, `/health`, `/ready`, and the DDL routes. Not the operator surface. A `Client`, an `iter.Seq2` walk over record ids, and a `database/sql` driver so `sqlx` and anything else that speaks it works. No dependencies. `go.mod` has no `require` block, there is no `go.sum`, and CI fails if either changes - `make deps` walks `go list -deps` and asserts every package is either stdlib or this module. What is ported rather than reinvented, because a client with its own idea of these agrees with the server until it does not: - `sql.go` is `contrib/big-message/src/sql.rs` function for function, with its test vectors. Identifiers are always quoted, numbers are checked against `big_sql::lex`'s own grammar, and `nil` is refused because the dialect has no `NULL` literal. One thing is shorter here: `strconv.FormatFloat` with `'f'` and precision -1 never emits an exponent, so `push_float`'s search for a spelling has nothing to search for. `1e300` is still refused, by the same `u64` overflow it reaches in Rust. - The retry rules and the connection lifetime from `big-message`'s readme. `TransportError.Sent` is the split: false is always safe to repeat, true is never repeated on a route that allocates. The connection is retired at 900 requests and 3s idle against the server's 1000 and 5s, so the idle-close race is unreachable rather than handled. Through `database/sql` the same two rules are `driver.Validator.IsValid` and `driver.SessionResetter.ResetSession`, which the pool already calls at exactly those two moments. Three things Go forces that the Python client did not have to answer: - `net/http` is not the transport. It knows whether a request reached the wire and discards the fact: when a request will not be retried, `transport.go` unwraps `nothingWrittenError` and `transportReadFromServerError` to the underlying error, so "nothing was written" and "written in full, then silence" arrive identical. That one bit is the whole retry policy. Its own retrying is not the problem and the readme says so. `WithDoer` takes an implementation for anyone who needs a proxy more than the distinction. - `encoding/json` decodes a number into `any` as a `float64`, and record ids are `u64`: `18446744073709551615` comes back as `...552000`. So every shape is a typed struct and the escape hatch for a field this build has not heard of is `json.RawMessage`, not `map[string]any`. Two tests hold it, one at each end of the pipe. - `Client` takes a lock. The Python client declares `threadsafety = 1` and does not, on the grounds that a lock serialises calls without saying so. In Go a client that cannot be shared will be shared anyway; the readme says it is one connection and points at `database/sql` for parallelism. `?database=` is folded into the table name rather than sent as a parameter alone. #18 made the parameter scope the data routes, but the DDL routes read the path only - so a client that set a database would `CreateTable` in the default one and `Import` into another, with a 200 at every step. A qualified path works on every route and is documented to win over a disagreeing parameter, so `WithDatabase` has one meaning across the whole client. The parameter is still sent; the permission check reads it. Testing is 3,700 lines against 4,100 of source, and the vectors are the repository's own so the two sides are held to each other rather than each to itself. The fake server is a raw `net.Listen` rather than `httptest.Server`, which cannot emit a `Content-Length` that lies or a response cut off mid status line - the cases that matter. Integration is behind `-tags integration` and skips with instructions when no `big` binary is found; it does not build one, because a `go test` that shells out to cargo fails outright without Rust. The integration suite found the schema snapshot bug fixed in #18 - a create that answered 200, a field route that answered 200, and a write that said the table did not exist. `table()` asserts the schema admits a table exists rather than trusting the status code, which is what caught it, and a regression test pins drop-then-create.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
clients/gowas an empty directory. The Python client landed in #19 and the same argument applies here: anyone reaching for bigdb from Go had to write their own HTTP and their own escaping, and escaping is where a mistake is a security bug rather than a wrong answer.archived/featurebaseis Go and is not a starting point - it speaks a different server's routes.Same scope as the Python client:
/sql,/table/{t}/query,/table/{t}/import,/table/{t}/delete,/table/{t}/records,/schema,/health,/ready, and the DDL routes. Not the operator surface. AClient, aniter.Seq2walk over record ids, and adatabase/sqldriver sosqlxand anything else that speaks it works.No dependencies.
go.modhas norequireblock, there is nogo.sum, and CI fails if either changes -make depswalksgo list -depsand asserts every package is either stdlib or this module.What is ported rather than reinvented, because a client with its own idea of these agrees with the server until it does not:
sql.goiscontrib/big-message/src/sql.rsfunction for function, with its test vectors. Identifiers are always quoted, numbers are checked againstbig_sql::lex's own grammar, andnilis refused because the dialect has noNULLliteral. One thing is shorter here:strconv.FormatFloatwith'f'and precision -1 never emits an exponent, sopush_float's search for a spelling has nothing to search for.1e300is still refused, by the sameu64overflow it reaches in Rust.big-message's readme.TransportError.Sentis the split: false is always safe to repeat, true is never repeated on a route that allocates. The connection is retired at 900 requests and 3s idle against the server's 1000 and 5s, so the idle-close race is unreachable rather than handled. Throughdatabase/sqlthe same two rules aredriver.Validator.IsValidanddriver.SessionResetter.ResetSession, which the pool already calls at exactly those two moments.Three things Go forces that the Python client did not have to answer:
net/httpis not the transport. It knows whether a request reached the wire and discards the fact: when a request will not be retried,transport.gounwrapsnothingWrittenErrorandtransportReadFromServerErrorto the underlying error, so "nothing was written" and "written in full, then silence" arrive identical. That one bit is the whole retry policy. Its own retrying is not the problem and the readme says so.WithDoertakes an implementation for anyone who needs a proxy more than the distinction.encoding/jsondecodes a number intoanyas afloat64, and record ids areu64:18446744073709551615comes back as...552000. So every shape is a typed struct and the escape hatch for a field this build has not heard of isjson.RawMessage, notmap[string]any. Two tests hold it, one at each end of the pipe.Clienttakes a lock. The Python client declaresthreadsafety = 1and does not, on the grounds that a lock serialises calls without saying so. In Go a client that cannot be shared will be shared anyway; the readme says it is one connection and points atdatabase/sqlfor parallelism.?database=is folded into the table name rather than sent as a parameter alone. #18 made the parameter scope the data routes, but the DDL routes read the path only - so a client that set a database wouldCreateTablein the default one andImportinto another, with a 200 at every step. A qualified path works on every route and is documented to win over a disagreeing parameter, soWithDatabasehas one meaning across the whole client. The parameter is still sent; the permission check reads it.Testing is 3,700 lines against 4,100 of source, and the vectors are the repository's own so the two sides are held to each other rather than each to itself. The fake server is a raw
net.Listenrather thanhttptest.Server, which cannot emit aContent-Lengththat lies or a response cut off mid status line - the cases that matter. Integration is behind-tags integrationand skips with instructions when nobigbinary is found; it does not build one, because ago testthat shells out to cargo fails outright without Rust.The integration suite found the schema snapshot bug fixed in #18 - a create that answered 200, a field route that answered 200, and a write that said the table did not exist.
table()asserts the schema admits a table exists rather than trusting the status code, which is what caught it, and a regression test pins drop-then-create.