Schultzer

Schultzer

It’s actually the same API! The only difference is that Ecto does not support executing multiple queries in one round-trip. AFAIK it actually is valid to send multiple SQL statements in one request, but it seems like nobody ever does that. I wonder why that is.

It’s common to execute multiple statements in a query. Ecto and Ecto.Query only supports that within a transaction. Doing this outside of a transaction with is also common would require you to write raw SQL and only myxql supports returning multiple values.

1
Post #51
garrison

garrison

Do you know if this is actually what happens? It seems like the C API allows you to create get() futures and then block on any (one) of them (or register a callback). The point at which you block seems like a great time to send out a batch, though of course it could also batch early if a lot of futures are created. There is a classic batching throughput/latency tradeoff here.

If all you do is register callbacks I don’t see how the client would know you want to block, though. So I guess it must just be a timeout after all? I feel like providing explicit control to the developer here is preferable, so I’m curious why they did it that way.

I should say for the record that I have spent almost zero time reverse-engineering the client because unlike the server side the client implementation is pretty obvious (sans the specifics, clearly).

Wait, it does? What is the syntax for that?

0
Post #52
Schultzer

Schultzer

When I execute multiple queries in a transaction, then I use Ecto multi, however that has become frown upon, for reasons I don’t find compelling. So today you would probably have to wrap all of them in anonymized function if you were to use Ecto. But for me, in the future I will be using SQL, which removes any limitations and abstraction.

2
Post #53
garrison

garrison

But Ecto.Multi still doesn’t execute multiple statements in one request, right? I meant something like this:

select * from users where name = 'bob';
select * from users where name = 'alice';
# ... and so on

That is, sending multiple (semicolon-separated) SQL statements in one round-trip to the server. Obviously there are ways you could constrain this particular example into one statement, but there are cases where that gets messy (if the queries were more complicated).

I am not aware of any way to do this with Ecto short of dropping down to raw SQL, and even then I’m not sure if multiple result sets are supported. TBH I had some trouble determining if Postgres even supports them, though I did not look that hard.

FDB is a little different than an SQL database because the idea is you’re supposed to write the high-level query planner stuff as a stateless client to the underlying datastore, so unlike with SQL we need to think a bit harder about round-trips. The batching is also difficult because there are actually many servers and choosing which ones to send the requests to is a little more complicated than you might think, plus you might want to make requests to multiple servers concurrently to save latency.

But in a parallel universe where things went differently, you might expect a syntax like this:

query1 = one(from u in Users, where: u.name == 'alice')
query2 = one(from u in Users, where: u.name == 'bob')
{alice, bob} = Repo.execute({query1, query2})

And as you can see this is essentially what @jstimps has ended up with, except he has to fight Ecto a bit to do it because it was not designed for this.

1
Post #54
Schultzer

Schultzer

Myxql supports multiple results Ecto.Adapters.SQL — Ecto SQL v3.14.0, but yeah you would have to write SQL, my memory might be off, but I could swear that the transaction would happen in one go. If not then theres another limitation to Ecto I wasn’t aware of.

1
Post #55
josevalim

josevalim

Creator of Elixir

Exactly. Ecto doesn’t support it through its API but it does have Repo.query where the adapter can do what it wants (including multiple queries). This is a feature we could add at the high level API if required though and contributions are welcome.

2
Post #56
jstimps

jstimps OP

Hi folks,

An update that’s not strictly EctoFDB related – I converted (/copied) the official FoundationDB Class Scheduling Tutorial to an Elixir-focused Livebook:

In the tutorial, we develop a simple data layer (using key-values in subspaces) that an application could use to sign up students for classes and drop those classes, adding some interesting business logic along the way.

The tutorial could be useful for anyone who wants to

  • Get started with :erlfdb itself
  • Understand the essentials of the EctoFDB implementation, which takes these same ideas and fits them to a subset of the Ecto abstractions
2
Post #57
garrison

garrison

Nice guide! Reading through it reminded me of something.

I had been considering for some time whether it would be a good idea to make the tuple encodings “follow” Erlang term order. Not exactly, but closer than the FDB tuples. The first step would be to rearrange the typecodes to match term order, which is easy enough.

But the problem is that Erlang tuples are sorted length-first rather than lexicographically, which is very bad behavior for something like FDB. However, during a conversation with @Asd in the Bedrock thread I realized that if you just avoid tuples and use lists instead you don’t have this problem because lists are ordered correctly (by their elements). Which makes perfect sense, because computing the length of a list would be very expensive. (It remains a mystery why tuples are compared in such an unhelpful manner, though.)

The irony is that I was implementing tuple encodings a couple of weeks ago following the :erlfdb_tuple implementation (out of laziness) and I noticed that the encoder/decoder uses lists internally and converts from/to tuples at the start/end, which makes perfect sense because you want to build up the list incrementally as you parse. And I thought “why bother, the lists will generally be short anyway”, so I just used lists in the API instead of tuples.

So as it turns out, I have actually already done this. By accident!

With erlfdb this is probably not something you want to change at this point (and you wouldn’t want to break compatibility with FDB tuples either), but I’m curious what you think about trying to follow term order with the encodings. It’s not something that actually matters in reality, but I find it oddly satisfying.

The performance of lists vs. tuples is an interesting question. In practice, I assume tuples are slightly faster:

[a, b, [c, d]] = Tuple.unpack(bin)
# vs.
{a, b, {c, d}} = Tuple.unpack(bin)

But for short lists I’m doubtful there is a meaningful difference. And using tuples costs some performance too because there is an extra conversion (:erlfdb_tuple builds up a list first). Is there any record of why erlfdb uses tuples over lists?

The integer/float comparison behavior seems like a bad path to go down, though, so I would still deviate from term order there I think.

0
Post #58
jstimps

jstimps OP

I haven’t seen one. Now that you mention it, I do agree that using lists in Erlang/Elixir (I’ll use “erts” for shorthand) would be more ergonomic. As you say, the FDB Tuple layer encourages the practice of “building up” a fdb-tuple, which is awkward with an erts-tuple, since they’re fixed length data structures.

Also, pattern matching on erts-lists is even more powerful than erts-tuples, so it sounds like very good idea indeed!

{"user", user_id, _, _, _, _, _, _}
  = :erlfdb_tuple.unpack(key) # ugh!

["user", user_id | _]
  = :erlfdb_tuple_v2.unpack(key) # yay!

I have a bad track record of predicting reality in micro-benchmarks like this, but I wonder if using erts-lists would actually be faster in the best case than erts-tuples due to the conversion you mention. Either way, it’s likely to be negligible compared to I/O :person_shrugging: .

I can see why this would be useful for your database server, likewise for Bedrock, since you’re more likely to want to do key comparisons with both the binary representation and the data structure. On the client, this has never been a pain point for me, though, since the server always returns keys in the correct sort order, and I can’t remember ever needing to compare them myself. That being said, I’m a big fan of design simplicity, so :+1: from me!

On the question of whether or not an ERTS-friendly Tuple V2 would be a good idea to store in actual FDB – I’m a maybe on this. On the one hand, FDB is supposed to allow the client to be entirely in control of the Layer.

On the other hand, GetMappedRange exists. As you’re aware, FDB implements this feature with assumptions about the key and value encoding, specifically that they are Tuple encoded. If you were to only change the type codes, it would probably still work because FDB would have no reason to decode the types. AFAIK their only assumption is regarding the boundaries between fields. But GetMappedRange is so finicky that I worry there is some dragon lurking there.

Keeping in mind how important records were (and still are) in Erlang, the tuple ordering makes sense. Having fixed-length tuples together means that different versions of your ets and mnesia records would be nicely grouped. I don’t know if this is the reason, but it seems like a real benefit.

(This also illustrates why records can be tricky :smiley: )

iex(1)> :ets.new(:tab, [:named_table, {:keypos, 2}, :bag])
:tab
iex(2)> :ets.insert(:tab, {:user, "1"})
true
iex(3)> :ets.insert(:tab, {:user, "2"})
true
iex(4)> :ets.insert(:tab, {:user, "1", "Alice"})
true
iex(5)> :ets.insert(:tab, {:user, "2", "Bob"})
true
iex(6)> :ets.tab2list(:tab) |> Enum.sort()
[{:user, "1"}, {:user, "2"}, {:user, "1", "Alice"}, {:user, "2", "Bob"}]

Wdym? If you’re going for a stable sort order in both binary and erts, you must, no?

2
Post #59
garrison

garrison

This was not my original motivation, but I could see it being handy. I haven’t written a higher-level layer yet so I don’t know exactly how often it comes up. Probably the reason you haven’t run into it is that you are specifically pushing responsibility for further filtering/sorting on to the user (as opposed to having a query planner), so you can always take advantage of FDB’s sorting. But I do wonder how often I will need to compare a keyset without having already encoded the underlying binary.

I know erlfdb has an FDB term comparator somewhere.

Well my original thinking was that if you always have a schema with fixed types this doesn’t matter. FDB presumably had the same thinking as they don’t mix floats and ints.

But, actually, I am very interested in providing a type system with union types, and it would be quite elegant if an index on an integer | float field could be built. I suppose you could still get away with coercing the integers into floats in the index.

I’m torn on this, I’ll have to look into it. The varint encoding from FDB is very elegant and useful so I wouldn’t want to lose that.

The sext library (yeah) seems to handle this by encoding a fraction after the integer part somehow. I’ll stare at it some more until I understand it.

Yeah, this is exactly the reason I think K/V is a bad abstraction (as discussed previously). I think at this point I’m just going to go for “record store” branding and skip the term “key/value store” entirely (even though the underlying internals are still ordered binary/binary). My only worry is people might then expect a schema, but I’ll work around that. “Schemaless record store?” “Semi-structured”? Idk.

2
Post #60