Fork me on GitHub
#xtdb
<
2021-09-05
>
emccue14:09:06

Okay so real basic code in sql

emccue14:09:11

(jdbc/with-transaction [tx db]
  (if-let [user (user/by-email tx "[email protected]")]
    {:failed true
     :reason :already-exists}
    {:failed false
     :user   (p-user/create! tx ...)}))

emccue14:09:58

In postgres this constraint "only one user per email" could be encoded directly in the db - thats not true with crux

refset15:09:52

Actually, you can achieve this directly by using IDs to hold/enforce the constraint, see https://opencrux.com/community/faq.html#uniqueconstraint

refset15:09:58

(though I appreciate it's not quite as obvious/ergonomic as with a predefined schema)

emccue16:09:25

Right so in this case I could put anything unique inside the ID for an entity

emccue16:09:01

instead of

{:crux.db/id #uuid ...
 :user/email "[email protected]"}
you do
{:crux.db/id {:user/id #uuid ...
              :user/email "[email protected]"}}

emccue16:09:50

or

{:crux.db/id {:type :com.company/user
              :user/email "[email protected]"}}

emccue16:09:09

this just falls apart with requirements like "a user should be able to change their email"

emccue16:09:35

since we rely on the id to be the identifier for the entity over time

emccue16:09:35

I'm still only 20% there on transaction functions (in crux and datomic), and i don't understand the semantics of match at all yet - but maybe thats the solution?

refset17:09:12

the solution implied by option (1) in that link looks more like:

{:crux.db/id "[email protected]"
 :user/current-owner #uuid abc}
{:crux.db/id #uuid abc
 :user/name "Bob"}

refset17:09:33

match is for ensuring that basic constraints hold during an optimistically submitted transaction, since you can't be certain about the ordering of concurrent transaction writes, whereas transaction functions execute serially (at the expense of some extra IO, therefore limiting max throughput and min latency)

emccue22:09:46

okay so unique things live as their own entities basically

emccue22:09:24

a user doesn't contain an email, an email just exists as a thing and has one owner

emccue22:09:42

which it manages

refset23:09:19

yep, that's it exactly 🙂

emccue23:09:43

Okay so if i wanted to query that and end up with {:user/name ... :user/email ...} the closest i've gotten is

(crux/q
  (crux/db crux)
  {:find '[(pull ?user-id [:user/name]) (pull ?email-id [(:email/value {:as :user/email})])]
   :where '[[?user-id :user/name "bob"]
            [?email-id :email/owner ?user]]})
=> #{[#:user{:name "bob"} #:user{:email "[email protected]"}]}

emccue23:09:44

which...eh? - i can certainly merge the two maps its just somethin

emccue23:09:07

sorry if this is annoying btw - its just a hard sell no matter what time-aware db we go with and I really want to have a full picture of everything

refset08:09:47

not at all, it's definitely not annoying, don't worry about that 😄 you can add :keys to get return maps which is a step closer

(crux/q
  (crux/db crux)
  {:find '[(pull ?user-id [:user/name]) (pull ?email-id [(:email/value {:as :user/email})])]
   :keys '[user/name user/email]
   :where '[[?user-id :user/name "bob"]
            [?email-id :email/owner ?user]]})
=> #{{:user/name #:user{:name "bob"} :user/email #:user{:email "[email protected]"}}}
but if you really want that specific shape I think you probably don't want to use pull at all
(crux/q
  (crux/db crux)
  {:find '[?user-name ?email-value])]
   :in '[?user-name]
   :keys '[user/name user/email]
   :where '[[?user-id :user/name ?user-name]
            [?email-id :email/owner ?user]
            [?email-id :email/value ?email-value]]}
   "bob")
=> #{{:user/name "bob" :user/email "[email protected]"}}

emccue14:09:01

but regardless with this sort of model "read this stuff and then write based on it" can enforce the constraint at the code level

emccue14:09:34

my understanding is that this cannot be done in general in crux

emccue14:09:02

i.e. - I need to either put this logic in a transaction function, which would probably be kosher for this case, but in our codebase where we want some sort of temporality we have blocks that are just db/in-transaction ... with arbitrary queries and such

emccue14:09:05

so what I would really want would be a way to say "I am starting a thing and if any of the facts I considered during the thing change before i'm done, make me restart"

refset15:09:28

Hi 🙂 Crux transaction functions can query the db arbitrarily and return ops that are guaranteed to not have had their data dependencies changed concurrently. I think that covers most of what you need, but when you say "make me restart" ...do you mean you want Crux to schedule another transaction for you, or that you expect to be to be able to re-attempt one from your code trivially?

emccue16:09:37

re-attempt trivially i suppose? Really i'm just looking for what is the find/replace for current semantics in our code

emccue16:09:42

if that tracks

emccue14:09:14

which is different from a "transaction" as crux defines it, i think

emccue14:09:11

and if i'm bumping up against fundamental limitations in crux thats fine - I just want to know where the lines are