Skip to content
This repository has been archived by the owner on Sep 2, 2022. It is now read-only.

Atomic operations #1349

Closed
sorenbs opened this issue Dec 1, 2017 · 27 comments
Closed

Atomic operations #1349

sorenbs opened this issue Dec 1, 2017 · 27 comments

Comments

@sorenbs
Copy link
Member

sorenbs commented Dec 1, 2017

GraphQL limitation and workaround

The most natural way to express atomic operations would be to allow them inline in any place where you would normally specify a value to set:

Set age to 7:

updateCat(
  where: {id: "..."},
  data: {age: 7}
)

Increment age by 1:

updateCat(
  where: {id: "..."},
  data: {age: {inc: 1}}
)

Unfortunately, this is not valid GraphQL.

[RFC] GraphQL Input Union type

There is an RFC trying to add union input types to GraphQL. It proposes using an extra field to specify the input type.

The example above updated according to the RFC could look like this:

Set age to 7:

updateCat(
  where: {id: "..."},
  data: {
    kind: Set,
    age: 7
  }
)

Increment age by 1:

updateCat(
  where: {id: "..."},
  data: {
    kind: SetAndAtomicAge, 
    age: {inc: 1}
  }
)

Increment age and lives by 1:

updateCat(
  where: {id: "..."},
  data: {kind: SetAndAtomicAgeAndAtomicLives, age: {inc: 1}, lives: {inc: 1}}
)

The added overhead of an extra field just to specify the type of the input object has a prohibitively negative impact on developer ergonomics.
Furthermore, it results in a combinatorial explosion as it should be possible perform atomic operations on multiple fields at the same time.

Workaround for missing union input type support

As we can not use a union input type, we will work around it by adding an extra field to all data input types:

updateCat(
  where: {id: "..."},
  data: {
    lives: 7
    _atomic: {
      age: {increment: 1}
    }
  }
)

The field is prefixed with _ in order to avoid clashing with a field on the type called atomic.

What does it look like

Atomic Operations for top level update mutation

The data argument receives an extra optional atomic field containing all model fields that are able to perform atomic operations.

Given this schema

type Cat {
  age: Int
  lives: Int
  born: DateTime
  name: String @unique
}

This will increment the age by 1:

updateCat(
  where: {id: "..."},
  data: {
    _atomic: {
      age: {increment: 1}
    }
  }
)

Multiple operations can be performed transactionally at once:

updateCat(
  where: {id: "..."},
  data: {
    name: "Super-Cat"
    _atomic: {
      age: {increment: 1}
      lives: {increment: 2}
    }
  }
)

Atomic Operations for top level upsert mutation

The update part of upsert is identical to the normal update mutation described above:

upsertCat(
  where: {name: "Super-Cat"},
  create: {name: "Super-Cat", lives: 7}
  update: {
    _atomic: {
      age: {increment: 1}
    }
  }
)

Atomic operations for nested update mutation

Supported both on one and many relations:

updateHome(
  where: {id: "..."}
  data: {
    masterCat: {
      update: {_atomic: {age: {increment: 1}}}
    }
    cats: {
      where: {name: "Super-Cat"}
      update: {_atomic: {age: {decrement: 1}}}
    }
  }
)

Atomic operations for nested upsert mutation

The update part of the upsert is similar to a normal nested update:

updateHome(
  where: {id: "..."}
  data: {
    masterCat: {
      upsert: {
        create: {name: "MasterCat"},
        update: {
          _atomic: {age: {increment: 1}}}
        }
    }
  }
)

Supported atomic operation

question: MongoDB uses shorthand notation for these operations (inc, mul, min, max). Should we do the same or rather use long form (increment, multiply, minimum, maximum)?

Int

  • inc add given value
  • mul Multiply by given value
  • min Set to given value if it is smaller than current
  • max Set to given value if it is larger than current

Float

  • inc add given value
  • mul Multiply by given value
  • min Set to given value if it is smaller than current
  • max Set to given value if it is larger than current

Boolean

  • flip Set to the opposite of current value. No change if null

Others

We could decide to implement something for DateTime and String, but this is out of scope for now

Precedence

If both an atomic operation and a normal set data operation is specified for a field the atomic operation it takes precedence. For example, this mutation increments the age by 1 instead of setting it to 7:

updateCat(
  where: {id: "..."},
  data: {
    age: 7
  }
  atomic: {
    age: {increment: 1}
  }
)
@sorenbs sorenbs self-assigned this Dec 1, 2017
@dihmeetree
Copy link

Very much looking forward to this functionality, as my app relies heavily on operations like adding/subtracting for managing balances on user types. Using a custom built mutex is such a pain and adds extra/unnecessary time to a mutation (plus can be unreliable at times).

Thanks for adding this issue/update!

@mfts
Copy link

mfts commented Mar 17, 2018

I look forward to using this as it reduces a lot of complexity when working with upsert functions where I need to add/subtract time balances to an item in the update part.

@mfts
Copy link

mfts commented Mar 20, 2018

Just an example of how much complexity these atomic operations would reduce in my case. Compare the two implementations

Model

type CapacityDowntime {
  capacityAE: Float!
  capacityAMH: Float!
  capacityAMS: Float!
  capacityAO: Float!
  capacityAT: Float!
  capacityAvor: Float!
  capacityNDT: Float!
  capacityPC: Float!
  capacityPainter: Float!
  capacityStans: Float!
  date: DateTime!
  downtime: Downtime @relation(name: "CapacityDowntimeOnDowntime")
  id: ID! @unique
}

type CapacityActualDay {
  id: ID! @unique
  capacityAE: Float!
  capacityAMH: Float!
  capacityAMS: Float!
  capacityAO: Float!
  capacityAT: Float!
  capacityAvor: Float!
  capacityNDT: Float!
  capacityPC: Float!
  capacityPainter: Float!
  capacityStans: Float!
  date: DateTime! @unique
}

Resolver

createCapacityDowntime2(
    capacityAE: Float!,
    capacityAMH: Float!,
    capacityAMS: Float!,
    capacityAO: Float!,
    capacityAT: Float!,
    capacityAvor: Float!,
    capacityNDT: Float!,
    capacityPC: Float!,
    capacityPainter: Float!,
    capacityStans: Float!,
    date: DateTime!,
    downtimeId: ID!): CapacityDowntime!

Resolver Implementation

Create or Update + Increment

async createCapacityDowntime2(parent, args, ctx: Context, info) {
    const { downtimeId, date, ...other } = args

    const existingCapacityActualDay = await ctx.db.exists.CapacityActualDay({ date })
    if (existingCapacityActualDay == true){
      const currentCapacityActualDay = await ctx.db.query.capacityActualDay({ where: { date }}, info)
      const capacityActualDay = await ctx.db.mutation.updateCapacityActualDay({
        where: { date },
        data: {
          capacityAE: currentCapacityActualDay.capacityAE + other.capacityAE,
          capacityAMH: currentCapacityActualDay.capacityAMH + other.capacityAMH,
          capacityAMS: currentCapacityActualDay.capacityAMS + other.capacityAMS,
          capacityAO: currentCapacityActualDay.capacityAO + other.capacityAO,
          capacityAT: currentCapacityActualDay.capacityAT + other.capacityAT,
          capacityAvor: currentCapacityActualDay.capacityAvor + other.capacityAvor,
          capacityNDT: currentCapacityActualDay.capacityNDT + other.capacityNDT,
          capacityPC: currentCapacityActualDay.capacityPC + other.capacityPC,
          capacityPainter: currentCapacityActualDay.capacityPainter + other.capacityPainter,
          capacityStans: currentCapacityActualDay.capacityStans + other.capacityStans,
        }
      }, info)
    } else {
      const capacityActualDay = await ctx.db.mutation.createCapacityActualDay({
        data: { ...other, date }
      }, info)
    }

   ....
}

VS

Upsert with atomic operations

async createCapacityDowntime2(parent, args, ctx: Context, info) {
    const { downtimeId, date, ...other } = args
    const capacityActualDay = await ctx.db.mutation.upsertCapacityActualDay({
      where: { date },
      create: { date, ...other },
      update: { atomic: { increment: {...other} } }
    }, info)

   ....
}

@sorenbs
Copy link
Member Author

sorenbs commented Mar 20, 2018

Thanks Marc!

This is exactly the kind of motivating use case that help us prioritise feature requests.

@Maxhodges
Copy link

any ETA on this? We also need auto-increment (I'm surprised this isn't more widely needed!) @sorenbs

@roycclu
Copy link

roycclu commented Jun 12, 2018

Any update on atomic operation? Right now the two solutions are

  1. Do a count on an aggregate function. Which traverses through an entire table, then sum.
    Downside: Performance is an issue.
  2. Get a record, then update the record.
    Downside: no ACID compliance.

This would be so simple in SQL, please find a way to implement. At least any timeline on this?

@kevinmarrec
Copy link

Any update on this ?

@sorenbs
Copy link
Member Author

sorenbs commented Nov 14, 2018

This continues to be an important feature for us. I'll update this issue when we have a concrete timeframe. See also this explanation for why we were unable to ship this feature in Q3 as planned.

@mcmar
Copy link

mcmar commented Nov 15, 2018

Hi, since we're already talking about a similar issue on another thread, maybe I can chime in here with a suggestion:

updateCat(
  where: {id: "..."},
  data: {
    age_increment: 1
  }
)

This is relatively simple and the use of _ should ensure that it never clashes with another field.
Same thing for _multiply or _minimum. Same precedence rules apply as above.

@sorenbs
Copy link
Member Author

sorenbs commented Nov 15, 2018

@mcmar - do you think this is cleaner than the proposed _atomic syntax?

One concern I have is that you will have a very long auto-complete list in your IDE containing all these atomic operations for each field.

@stale
Copy link

stale bot commented Dec 30, 2018

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 10 days if no further activity occurs. Thank you for your contributions.

@stale stale bot added the status/stale Marked as state by the GitHub stalebot label Dec 30, 2018
@adammichaelwilliams
Copy link
Contributor

@sorenbs Any update on this?

@stale stale bot removed the status/stale Marked as state by the GitHub stalebot label Jan 2, 2019
@stale stale bot added the status/stale Marked as state by the GitHub stalebot label Feb 24, 2019
@prisma prisma deleted a comment from stale bot Feb 27, 2019
@stale stale bot removed the status/stale Marked as state by the GitHub stalebot label Feb 27, 2019
@gabrieltong
Copy link

any ETA ?

@SoufianeX
Copy link

any updates to this ? is it available ?

@dreamniker
Copy link

any updates?

@soqt
Copy link

soqt commented Apr 15, 2019

Hope this could be resolved soon.

@impowski
Copy link

Any update on this? Would be amazing to have this one.

@wleite
Copy link

wleite commented Jun 28, 2019

Keep the thread alive. Any news about this? Thanks

@GemN
Copy link

GemN commented Jul 5, 2019

Need this as well

@pretor
Copy link

pretor commented Jul 30, 2019

I hope this will be implemented soon.

@AhmedKorim
Copy link

looking forward seeing this soon, what is the best working around for now?

@Pkmmte
Copy link

Pkmmte commented Aug 23, 2019

I hate to spam you guys, but I'm also looking forward to this feature.

I've been watching this thread for more than a year now hoping it arrives soon. It has stopped my company from releasing certain features on Prisma various times.

@TsumiNa
Copy link

TsumiNa commented Sep 16, 2019

Push it to hope this will be implemented soon.

@TSTsankov
Copy link

I hope this will get implemented by 2020..

@eg9y
Copy link

eg9y commented Oct 12, 2019

☹️

@jpandl19
Copy link

Are there any recommended workarounds? I've seen people grabbing the last result and incrementing it, but that could still lead to problems if multiple requests come in at the same time. I also saw this library implementing a mutex strategy for graph-cool, but it's out of date now: https://github.com/kbrandwijk/graphcool-mutex

Any other recommendations?

@steebchen
Copy link
Contributor

For everyone who wants to see this feature for Prisma 2, please upvote the respective issue for atomic operations in Prisma 2.

@janpio janpio closed this as completed Sep 1, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests