Skip to content

Collections:#19

Open
DeanPDX wants to merge 6 commits intodatastax:mainfrom
DeanPDX:collection-update-many
Open

Collections:#19
DeanPDX wants to merge 6 commits intodatastax:mainfrom
DeanPDX:collection-update-many

Conversation

@DeanPDX
Copy link
Copy Markdown
Contributor

@DeanPDX DeanPDX commented Mar 26, 2026

Collection Update Updates

Hey @toptobes check this out. This implements collection updateMany. I also didn't like that the "update" param was of type any so I took a stab at helpers for updates, along with unit tests that verify my structs marshal to the JSON from the curl command in the docs.

So this:

u := update.U{
	"$set": map[string]any{
		"color":   "blue",
		"classes": []string{"biology", "algebra", "swimming"},
	},
	"$unset": map[string]any{
		"phone": "",
	},
	"$inc": map[string]any{
		"age": 1,
	},
}

... is the same as this:

u := update.Set("color", "blue").
	Set("classes", []string{"biology", "algebra", "swimming"}).
	Unset("phone").
	Inc("age", 1)

And both properly json.Marshal into this example from the docs

DeanPDX added 4 commits March 26, 2026 11:27
- Implement updateMany.
- Add Update interface with fluent API.
@DeanPDX
Copy link
Copy Markdown
Contributor Author

DeanPDX commented Mar 27, 2026

This closes #18.

@toptobes
Copy link
Copy Markdown
Collaborator

(I assume you meant for type U map[string]any to implement MarshalJSON() but you just haven't gotten around to it?)

@toptobes
Copy link
Copy Markdown
Collaborator

toptobes commented Mar 28, 2026

Annnyyyyywayyyyyy so I do like the idea of having offiicial typed and untyped ways of defining your update filters, it's definitely better than just any.

There's just one problem, which is that tables and collections have somewhat different update filters, with collection updates allowing for much more functionality than table updates, so it'd probably be in your best interest to split Update into CollectionUpdate and TableUpdate.

Now if you'd rather just go ahead and just use the same Update stuff for both collections and tables regardless you can disregard the following, but if you'd rather play it more type-safe, read on.

So if you've read on, humor me for a minute and consider the following:

package update

import "encoding/json"

// INTERFACES

type CollectionUpdate interface {
	json.Marshaler
	isCollectionUpdate() // brand to make the interface more nominal
}

type TableUpdate interface {
	json.Marshaler
	isTableUpdate() // brand to make the interface more nominal
}

// COLLECTION UPDATE

var _ CollectionUpdate = (*CollectionUpdater)(nil)

func Coll() *CollectionUpdater {
	return &CollectionUpdater{ops: make(map[string]map[string]any)}
}

type CollectionUpdater struct {
	ops map[string]map[string]any
}

func (u *CollectionUpdater) MarshalJSON() ([]byte, error) {
	return json.Marshal(u.ops)
}

func (u *CollectionUpdater) isCollectionUpdate() {
	// no-op, just to satisfy the interface
}

// TABLE UPDATE

var _ TableUpdate = (*TableUpdater)(nil)

func Table() *TableUpdater {
	return &TableUpdater{ops: make(map[string]map[string]any)}
}

type TableUpdater struct {
	ops map[string]map[string]any
}

func (u *TableUpdater) MarshalJSON() ([]byte, error) {
	return json.Marshal(u.ops)
}

func (u *TableUpdater) isTableUpdate() {
	// no-op, just to satisfy the interface
}

// GENERIC UPDATE

var _ CollectionUpdate = (*U)(nil)
var _ TableUpdate = (*U)(nil)

type U map[string]any

func (u U) MarshalJSON() ([]byte, error) {
	return json.Marshal(map[string]any(u))
}

func (u U) isCollectionUpdate() {
	// no-op, just to satisfy the interfaces
}

func (u U) isTableUpdate() {
	// no-op, just to satisfy the interfaces
}
package astradb

import (
	"github.com/datastax/astra-db-go/update"
)

func CollectionUpdateMany(u update.CollectionUpdate) {
	// pretend this actually does something
}

func main() {
	// ok
	CollectionUpdateMany(update.Coll())

	// also ok; less type safe, but can be used for both table and collection updates
	CollectionUpdateMany(update.U{})

	// Cannot use update.Table() (type *TableUpdater) as the type update.CollectionUpdate
	// Type does not implement update.CollectionUpdate as some methods are missing:
	// isCollectionUpdate()
	CollectionUpdateMany(update.Table())
}

My solution takes inspiration from opaque/branded types in TypeScript (which also has structural interfaces) as a way to produce somewhat nominal interfaces.

  • Of course the interfaces aren't actually nominal, but at least you can't accidentally use a random interface in place of *Update that just happens to also implement json.Marshaler
  • If you want users to be able to implement their own *Update types, you can make the is*Update() methods public so users can also implement them

The idea is that instead of doing update.XYZ() or update.New().XYZ(), you would do update.Coll().XYZ() or update.Table().XYZ()

  • Or update.Collection().XYZ(). Up to you.

That way, both CollectionUpdate and TableUpdate can safely have their own builder methods without collisions or extra methods from the other usages perspective.

  • note that I am still slightly sick as I write this so what I just said somehow makes sense in my own head

@toptobes
Copy link
Copy Markdown
Collaborator

This would unfortunately mean you couldn't really do a direct update.Set() or something unless that returned an update.U{} (which is both a CollectionUpdate and TableUpdate), but that wouldn't be super intuitive IMO

@toptobes
Copy link
Copy Markdown
Collaborator

toptobes commented Mar 28, 2026

In general, you don't need to have nominally different *Update types, but this pattern of using "nominal" interfaces could be used in various places in the client to prevent accidental usage of types that just happen to implement an interface which is really just another wrapper around json.Marshaler or something, such as with just normal filters.

Or of course if you wanted you could do something like

type CollectionUpdate interface {
	MkCollectionUpdateMarshaler() *json.Marshaler
}

or just

type CollectionUpdate interface {
	MarshalCollectionUpdateToJSON() ([]byte, error)
}

to also make the interfaces harder to accidentally mix up but that may take a little extra care and effort to create or use than just having a couple of no-op marker/branding methods like isCollectionUpdate()

@DeanPDX
Copy link
Copy Markdown
Contributor Author

DeanPDX commented Mar 30, 2026

Hey @toptobes I added more tests and split out collection/table updates. Give this a try and LMK what you think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants