prisma1 icon indicating copy to clipboard operation
prisma1 copied to clipboard

Atomic operations

Open sorenbs opened this issue 7 years ago • 27 comments

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 avatar Dec 01 '17 10:12 sorenbs

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!

dihmeetree avatar Dec 02 '17 17:12 dihmeetree

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 avatar Mar 17 '18 19:03 mfts

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)

   ....
}

mfts avatar Mar 20 '18 09:03 mfts

Thanks Marc!

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

sorenbs avatar Mar 20 '18 09:03 sorenbs

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

Maxhodges avatar May 08 '18 10:05 Maxhodges

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?

roycclu avatar Jun 12 '18 09:06 roycclu

Any update on this ?

kevinmarrec avatar Oct 10 '18 08:10 kevinmarrec

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.

sorenbs avatar Nov 14 '18 13:11 sorenbs

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.

mcmar avatar Nov 15 '18 03:11 mcmar

@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.

sorenbs avatar Nov 15 '18 08:11 sorenbs

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[bot] avatar Dec 30 '18 10:12 stale[bot]

@sorenbs Any update on this?

adammichaelwilliams avatar Jan 02 '19 23:01 adammichaelwilliams

any ETA ?

gabrieltong avatar Mar 12 '19 02:03 gabrieltong

any updates to this ? is it available ?

SoufianeX avatar Apr 01 '19 21:04 SoufianeX

any updates?

dreamniker avatar Apr 11 '19 20:04 dreamniker

Hope this could be resolved soon.

soqt avatar Apr 15 '19 05:04 soqt

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

impowski avatar Apr 23 '19 13:04 impowski

Keep the thread alive. Any news about this? Thanks

wleite avatar Jun 28 '19 16:06 wleite

Need this as well

GemN avatar Jul 05 '19 13:07 GemN

I hope this will be implemented soon.

pretor avatar Jul 30 '19 07:07 pretor

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

AhmedKorim avatar Aug 18 '19 19:08 AhmedKorim

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.

Pkmmte avatar Aug 23 '19 00:08 Pkmmte

Push it to hope this will be implemented soon.

TsumiNa avatar Sep 16 '19 08:09 TsumiNa

I hope this will get implemented by 2020..

TSTsankov avatar Oct 09 '19 10:10 TSTsankov

:frowning_face:

eg9y avatar Oct 12 '19 17:10 eg9y

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?

jpandl19 avatar Nov 15 '19 03:11 jpandl19

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

steebchen avatar Jun 10 '20 15:06 steebchen