You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ac0a38f
Browse filesBrowse the repository at this point in the historyBrowse files
feat(golang): an optional sealed field, declared over a pointer
A sealed field may now be declared over a pointer to one scalar
(`DOB *encrypt.Date` with `stash:"dob,encrypt_into=Date"`, which #1293
reproduced being refused as a composite), and Decrypt gives nil back for a
nil (#1298).
A new tag part, `null=sealed` or `null=sql`, says how nil is stored, and
reaches the engine as the data plan's "null":
- sealed: nil is sealed as a typed null as long as a value, so the column
does not show which rows are absent. stashgen picks it for a field with
no index whose type has a fixed width, sealed with encrypt or into a
storage-only EQL type.
- sql: nothing is stored, so the field's columns hold SQL NULL and its
member in the generated type is a pointer. A field with an index, or of
text or bytes, has to say so: a null has no term yet, and text shows its
length, so stashgen will not choose a visible NULL silently.
gensupport gains Declaration.Null, Deref and GetPtr; the record model and
the encrypt record path carry the mode and accept a SQLNull field left
out. A stored null read into a field that is not a pointer is a Decrypt
error, from the engine's kind check. -model does not map a null=sql field
yet, and stashgen says so.
The reproduction from #1293 is folded into cmd/stashgen's tests, with the
guest-built skip the other engine test has.
Closes#1293.
Refs #1298.
Co-authored-by: Jonathon Walker <1989282+jonathon-walker@users.noreply.github.com>
Claude-Session: https://claude.ai/code/session_01XQN1QpaWVDGjHEg39KEJF3
`eql-bindings` (the Rust crate released with this package): a storage-only EQL v4 type (`Date`, `Integer`, `Text`, and the other types with no term) now also implements `EncryptFrom` and `DecryptInto` for an `Option` of its plaintext. A `None` seals as a typed null as long as a value (vitaminc 0.8), so an optional column does not show which rows are absent, and opens as `None`. A value seals exactly as before. The by-name dispatch (`encryption::targets`) reads and opens these types through the `Option`, so a null frame encrypts and a stored null decrypts to a null; a type with a term still refuses a null. eql-bindings' own `Date`, `Timestamp` and `Decimal` plaintexts declare their fixed width to vitaminc, so their `None` is padded too. The SQL bundle and the TypeScript types do not change.
Copy file name to clipboardExpand all lines: languages/golang/cmd/stashgen/README.md
+21-2Lines changed: 21 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -75,6 +75,7 @@ The first part of a tag is the field's name, which is the column name in a datab
75
75
|`stash:"tenant,context_field"`| the field's value is the context of every other field in the struct, in place of a `context=` tag; stored as it is |
76
76
|`stash:"-"`| leave the field out |
77
77
|`` _ struct{} `stash:"context=documents,opaque"` ``| seal the struct as one value |
78
+
|`stash:"born,encrypt_into=DateEq,null=sql"`| on a field declared over a pointer (`*encrypt.Date`): how it stores `nil`, `null=sealed` or `null=sql`; see "Optional fields" |
78
79
79
80
A struct has one context: either a `_ struct{}` field with `context=`, or one field with `context_field`, not both.
80
81
A `context_field` is a `string` whose value is a label such as `"tenants/acme"`, segments separated by `/`.
@@ -105,6 +106,24 @@ These words are the same as the Rust API's words for the same behaviour, with tw
105
106
`opaque` seals the whole struct as one `bytes` field holding a JSON document (see "What crosses the binding").
106
107
`json` names the JSON index, which the Rust API does not declare yet; this build refuses it.
107
108
109
+
### Optional fields
110
+
111
+
A sealed field declared over a pointer to one scalar (`*encrypt.Date`, `*int32`, `*string`, a pointer to a type defined over a scalar) is optional: `nil` is a value it may hold, and `Decrypt` gives it back as `nil`.
112
+
`null=` says how the field stores `nil`. There are two ways, and they differ in what a reader of the database can see:
113
+
114
+
|`null=`| Stored for `nil`| What a reader of the column sees |
115
+
|---|---|---|
116
+
|`sealed`| a sealed null, as long as a sealed value of the field's type | nothing: an absent value looks like any other |
117
+
|`sql`| nothing: the field's columns are `NULL`, and its member in the generated type is `nil`| which rows have no value |
118
+
119
+
A sealed null hides that the value is absent only where its length matches a value's and nothing beside it shows it, so it is the default for a field with no index whose type has a fixed width: a `bool`, a number, `encrypt.Date`, `time.Time`, `encrypt.Decimal`, `encrypt.Int128` and `encrypt.Uint128`, sealed with `encrypt` or into an EQL type with no index (`Date`, `Integer`, `Boolean`, ...).
120
+
A field with an index, and a field of text or bytes, has to write `null=sql`: a null has no index term yet, and text shows its length, so a sealed null would hide nothing there, and `stashgen` will not choose a visible `NULL` for you.
121
+
`null=sealed` on such a field stops `stashgen` with the reason.
122
+
123
+
A `null=sql` field's member in the generated type is a pointer (`*eql.DateEq`, `*EncryptedUserScore`), `nil` when the value was; a `null=sealed` field's is the same type as a non-optional field's, and its column is never `NULL`.
124
+
A stored null read into a field that is not a pointer is an error from `Decrypt`, naming the field, never a zero value.
125
+
`-model` does not map a `null=sql` field yet, and `stashgen` stops if it is asked to.
126
+
108
127
An embedded struct of your own adds its tagged fields to the outer struct.
109
128
An embedded struct from another package takes one tag for all of its fields: `stash:",passthrough"` stores them as they are, and `stash:"-"` leaves them out.
110
129
Tags for other libraries, such as `gorm`, `db` and `json`, are copied to the same field of the generated type.
@@ -250,10 +269,10 @@ Generated code sends the engine every sealed field with its value, under the dec
250
269
A passthrough field stays on the host: the engine does nothing to it a program could observe, and the FFI codec cannot carry every Go type a program stores beside a ciphertext (a `driver.Valuer`, a `sql.NullTime`).
251
270
An `opaque` struct crosses as one JSON document and is one column, decoded back into the exact Go types the struct declares; a field may be any type `encoding/json` carries both ways (a scalar or a type defined over one, `[]byte`, slices, maps with string or integer keys, pointers, structs whose fields are all exported, `time.Time`).
252
271
A float in an opaque struct must be finite: `encoding/json` refuses NaN and the infinities, so `Encrypt` fails for the batch with `encrypt.ErrEncoding`. A sealed float field outside an opaque struct carries them.
253
-
A sealed field outside an opaque struct is one scalar, or a type defined over one; a struct, slice or map field seals only inside an opaque struct.
272
+
A sealed field outside an opaque struct is one scalar, a type defined over one, or a pointer to either (see "Optional fields"); a struct, slice or map field seals only inside an opaque struct.
254
273
The scalars are `string`, `[]byte`, `bool`, every integer type (each sealed at its own width, `int` and `uint` as 64-bit), `encrypt.Int128` and `encrypt.Uint128`, `float32`, `float64`, `encrypt.Decimal`, `encrypt.Date` and `time.Time` (a timestamp). The wire type `stashgen` writes for each is the field's own: `int8` is `gensupport.Int8`, `time.Time` is `gensupport.Timestamp`, and so on.
255
274
An order index (`ore`, `ope`) on a decimal, date or timestamp is refused: those order terms wait for new cllw-ore and ore-rs releases (#1236).
256
-
The zero `encrypt.Date` is not a date: a date field must be set, and `Encrypt` refuses a zero one with `encrypt.ErrEncoding`, naming the field. A sealed field cannot be a pointer (`*encrypt.Date`is refused as a composite), so an optional date has no sealed form yet: keep it in an opaque struct, or store a sentinel date.
275
+
The zero `encrypt.Date` is not a date: a date field must be set, and `Encrypt` refuses a zero one with `encrypt.ErrEncoding`, naming the field. An optional date is a `*encrypt.Date`field (see "Optional fields").
257
276
A nil `[]byte` in a sealed field, or a nil slice of a type defined over `[]byte`, comes back from `Decrypt` as an empty, non-nil slice; inside an opaque struct, nil comes back as nil.
258
277
A protobuf message with a `oneof` cannot be generated from a policy: protoc-gen-go puts its members in wrapper types, not in the message struct, and `Generate` refuses it.
0 commit comments