 Welcome to PGX, Built Postgres Extensions with Rust. I'm joined by our speaker, Eric Ridge, president of ZomboDB who will discuss PGX's extension development workflow and its approach to Rust create organization. Rust types is Postgres types, UDFs and unit testing. My name is Lindsay Hooper. I'm one of the Postgres conference organizers and I'll be your moderator for this webinar. You can find me under the host as Postgres conference. Let me give you a little bit of a background about your speaker. Eric Ridge, as I said, is the president of ZomboDB. He has more than 20 years of professional software development experience across various industries and nearly all of that time has been spent using Postgres dating back to version 7.1. With that, I'm gonna hand it off to Eric. Take it away. Thanks, Lindsay. Hi, I'm Eric Ridge. It is awesome to be here. I am avoiding checking to see how many people have actually joined, so assuming there's more than just me and Lindsay, I'm glad everybody's here. And what I wanna show everybody today is the new framework that we've been developing to allow us as Postgres programmers to create extensions for Postgres and Rust instead of the traditional method of writing these things in C. And this framework is called PGX, mainly because that's pretty easy to type. And we started development on it about a year ago as around November of 2019. And the development of that started because we wanted to port our other Postgres extension called ZomboDB, which is a Postgres extension unless you use Elasticsearch as if it was an index in Postgres akin to like a B-Tree index or a GEN or GIST index, but backed by Elasticsearch. We wanted to port that to Rust for a couple of reasons, primarily to give us compile time guarantees of memory safety. That was the big reason. The thought process there was if you're running custom code inside Postgres, you kind of want some good guarantees that that code's not gonna crash. And Rust actually gives us that at compile time. In addition to making it easier to maintain that extension going forward and supporting multiple versions of Postgres relatively easily on top of the same code base. So in order to do that, we had to develop PGX first. And so we've kind of been developing over the course of the past year, I guess we've developed those things concurrently and we're at a point today where PGX is a nice usable framework on its own. And some of the things that PGX is designed to solve is kind of the development workflow of developing Postgres extensions. I don't really want to go down a rabbit hole of what that looks like in C, but in general, if you were to create a Postgres extension in C, you would find a small existing project. You would find a small existing Postgres extension in C. You would copy that project as a template. You'd go through and rename it to whatever you want your extension to be called. And then you'd slowly start to learn Postgres internals to start building up your extension in C. And so PGX wants to solve these problems for you up front. One of the primary things that PGX has is a sub-command for cargo. Cargo is Rust's build system. There's a sub-command called PGX. We're not going to go through everything here, but I do want to look at a couple of its options. The first one that you need to do when using PGX is cargo, PGX, and NIT. And what this does, and I'm gonna pop up a screenshot of what this does because it takes a few minutes to do what it does, but it downloads three versions of Postgres right now, the three that we support, 10, 11, and 12. It un-tars them, it configures them, it compiles them, it installs them locally in your user's home directory, and it validates that those installations are correct. And so those then become Postgres installations that are running on your computer in your home directory, that cargo PGX itself is managing for you throughout your development workflow. And we'll get into actually using cargo PGX here in just a second when we start writing some code. The other nice thing that cargo PGX does for you I've already made this, but we'll look at it anyway. We can make a new extension, and we'll just call it example, and it's kind of an instant sort of thing. And in there, it sets up a small Git repo with everything that you need to ultimately run your Postgres, run your extension on a particular version of Postgres. So by doing cargo PGX new and compiling it, or cargo PGX run, which compiles our extension and then it installs it into this privately managed version of Postgres for us, in this case Postgres 12. And we don't have an extension yet, but we can create it and it has one sample function. So we can just look at that real quick and that just says hello example. So cargo PGX new gets you bootstraps with getting that initial extension template created. That's a pretty nice feature. The other, well, we'll get into it a little bit more here, but just to show you, it sets up your cargo.toml file, your cargo.toml file, which is a configuration for how to build your project. And then in the source directory, you get a lib.rs with a very simple extension to get started. So let's actually do some programming here with this and start walking through what some of this stuff does. I'm gonna be flipping back and forth between a project directory for this talk and I'm using IntelliJ-C-Lion to edit code. It has some real nice Rust features for understanding Rust. So here we are in lib.rs. This is our main entry point for our Rust library extension. And first of all, I don't know who's watching knows a lot about Rust and who doesn't. So I'm not gonna spend too much time trying to demystify Rust, but maybe just a little bit here. So we're gonna use everything from the PGX crate. We'll get to mod phone number. That's another module that I'm gonna talk about here a little bit. PG module magic sets up some, it sets up an internal function inside the shared library that gets created for the extension so that when Postgres loads that library, it can validate that it indeed loaded an extension library and not some random malicious library. And so first and foremost, we start with a hello world function. This is a function whose name is hello underscore Postgres comp. It returns the static string. And this is the static string that we're returning with Rust. This would be akin to that might look more familiar if you're not a Rust programmer. Pretty much generally the last line or the last entry point in a function doesn't need a return or a semicolon. It'll just return that value. So just to take a look at that, we'll do cargo PGX run and on Postgres 12. And I don't recall if I had that extension in there, I don't, I guess we could just look. So create extension Postgres comp. And if we look, we now have, well, please ignore the phone number stuff for right now. We now have one function our hello Postgres comp function. And so Postgres comp. If we look at the results, there it is. Behind the scenes. So there's this attribute that we apply to functions that we want to use through SQL as called PGX turn. And it has a number of options that can be specified that we're not really gonna look at today. But behind the scenes, what PGX turn is doing is it's figuring out what the arguments, what the arguments and argument types of your function are, in this case, none. And what its return type is, and it's transparently doing all the translation of the underlying Postgres data type to the Rust data type and then back again from Rust back to Postgres. It does all of that for you. And it ensures that everything is checked at compile time. And we'll, let's write a little function here. We're gonna call it to lowercase and we're gonna take in an input string. We're gonna take in a reference to a string and we're gonna return an own string. And long and short of it is the LKs version of this, there's a, in Rust, there's a function on, on Refters and Serves called to lowercase and it returns a string. So we could just return that and to make it a little more rusty, we can, we can just do this. And so we've added a new function here and that means we need to rerun our extension. So that means compiling and going back into psql. So cargoPGX takes care of that for us. And since we added a new function, we have to drop and recreate our extension so that that new function will be picked up. So drop extension postgres comp, create extension postgres comp. And so now we have, it's down here at the bottom, but we have our to lowercase function. And you can see that it has a result data type of text and an input or it's arguments. It's one named input whose type is text. So behind the scenes, pgx and this pgx turn macro figured out, well, this, this roster really maps to postgres text, the postgres text type and so does rusts capital string, its own string type. So we can select to lowercase world and we get the result there that we would expect. Another interesting thing, and I think this is probably the coolest thing about pgx is that it generates the SQL for you. Notice we haven't written any SQL, well, we haven't written any like create function statements or anything, pgx generates all that for us. So if we look at, let me reformat this. This is in the SQL directory, lib.generated.sql. It's named after the rust file. So we have two functions, our original hello function here and then our to lowercase pgx figured out for us that it needs to be of the input is of type text and it returns text. And what might also be interesting is that it's set the function to be strict. And in postgres, what that means is that if a call to that function contains a null argument, then the results of that function is going to be null and postgres actually uses that information during its query planning, rewriting phases to like completely allied calling the function if it can detect that it's strict and has an argument of null. So if we go back to psql here and we try to just lowercase null, well, we get null back, right? But what's interesting about this is, so lowercase. So info will log a little message for us in the terminal. So what's interesting about this is if we do select to lowercase on hello world, we get our info message that says we are converting hello world to lowercase. But if we do it on null, we don't get an info message. Our function isn't called at all. And that's really kind of great because Russ doesn't, Russ doesn't really have the concept of nulls, what it does have, and we'll just rewrite this function a little bit. Let's say we do want that to be called when null is provided as the value. So we can say we want an option of string. And now we need to change that to debug output real quick. And so what we have now is an option that might contain a refster, might contain some refster or it might contain none, which would be the null case. And so the way that we might look at that is we would match input and we would say, if we have some s, then we want to return s.to lowercase. But then what do we do if we don't have anything? Anything. Cause we're, Russ is still requiring us to return a full, a fully owned string. So maybe we just return and we'll just literally return a string, let's do this, input was null. And so if we rerun our extension here and like come back and look at our generated lib, now the strict is gone from to lowercase. So now we can accept nulls, but we need to drop and recreate our extension because we changed the schema. So now hello world again, we're converting some of hello world to lowercase. But if we look at null, now it says we are converting none to lowercase. The return type wasn't null and it instead returns input was null. And this is one of those nice types, type safety features that Russ gives us that PGX is able to translate into postgres for us really well. I'd like to show a couple more interesting things, a couple more interesting examples with creating functions and dealing with data types. And then I want to move into looking at creating our own data type that contains operators and allows us to create indexes on it and search for that particular data. But if there's questions at this point, I'm happy to entertain. I feel like maybe I'm going a little quickly. Happy to answer any questions. One question that literally just came in is what, yeah, what advantage does creating our Rust extension have over the transitional C extension? So to answer John Ballard's question about the advantages, the primary advantage is the compile time memory safety and thread safety. Rust makes it a lot easier to do what's called feature gating to optionally compile code for different versions of Postgres. Like for example, we could also add CFG feature equals PG10. And then this function would only exist if we were using Postgres 10, if we were compiling for Postgres 10. I'm not going to do that here, but so and those are some major advantages from like a practicality standpoint, I guess. Rust is also a lot more, in my opinion, it's a lot more fluent than that be for PG10 plus then. No, that would have just been for Postgres 10. If you wanted to get more advanced and say, well, I want this just for any of PG10 or 12, you could write it like that. You know, I find Rust to be a lot more expressive. It's almost as easy as a, it's almost like a scripting language in a lot of ways. And I want to show a couple of examples of that. And WorldsDB had another question of, is it possible to apply PG extern directly to str.c lowercase? No, you would need to wrap it. That'd be an interesting thought process to work out how maybe we could do that. But no, you've got to at least directly wrap it. So let's look at, let's look at arrays real quick. Let's make a PG extern function. And in fact, I'm going to get rid of this. We're going to make a function. We're going to call it sum array. And we have our input. There's a couple of ways that we can write this, but we're just going to write it as a VEC. VEC is short for vector in Rust, akin to a list or like a list or like an array list in Java, maybe. Of I64s, Rust's 64 bit signed integer type maps to Postgres's big int type. And we want to return an I64. You know, one way to do this, right? It would be for I in inputs sum plus equals I, and then we could return sum. Right, I know that might be how you might set out to write this, but Rust makes things so much prettier. We can convert input into an iterator and just sum it. And then that's equivalent to that. And so let's compile and run our extension again. And again, because we added a new function, we need to drop and recreate our extension. And if we look at the function here, we can see that sum array's input is a type big int array and it returns a big int. So pgx is taking care of all this SQL for us. And here's our sum array. You can see it's written out here. So select sum array, array of one, two, three, that should be six, right? Yeah, so fairly straightforward. Again, these are just some little toy examples, but stable if you're sure your code passes the safe. Yes, that's a great conversation and chat there. So sum array's pretty straightforward. Again, what happens if there's a null, like if we put a null in here? So our functions declared to have a VEC of I64s, that means we can't have a null. Rust won't let us have one. It has no way to represent an I64 as a null. So what happens? Is it gonna crash? What's the deal? pgx is gonna figure that out for us. And it's gonna tell us that an array element was null. And so it just, it errors out that function. It would abort your transaction at that point. That call would be dead in the water in this particular case. But similar to above, we could take an option of I64s. And then what we would do is we would map the individual elements, E, we'll call it I, in input, to, we could do unwrap or say zero. And then return the sum of that. This would be kind of a rusty way of doing it. So if we have sum of I, we'll use that. Otherwise, we'll just use zero. And I'll just show that real quick. And then I wanna move on to set returning functions. These things are really neat. So sum array with our null in it now, this will still return six because we're gonna treat that null as zero now. And I'm pointing this stuff out. So like, if you're writing an extension in C, I don't really wanna get into what the declarations of the functions in C would look like. But it's kind of like, it's an SQL definition of that function first, and then you have to code your C implementation for that function to match what the SQL definition is. And if you mess up these little details around strict and what arguments can be null and what can't, you're leaving your Postgres server open for being open to at least crashing, right? And I would think if you're writing Postgres extensions in C, you're probably a pretty good C programmer anyway, but PGX's approach is the opposite, right? We declare and rust with our function signature what we want the SQL side to look like and everything matches up. So real quick, I wanna show one other small example here. We're gonna replicate Postgres as a generate series function for at least integers, big ints. So we're gonna have a start, which would be an I64, excuse me. An end that would be an I64. And we're gonna have a step that might as well also be an I64, excuse me, but we want to default this is a PGX macro. We wanna default the value of step to one. And then we are going to return an implementation of a Rust standard iterator whose item, Rust can be a little wordy whose item is of type I64. And so we're gonna build a Rust range from start to end inclusive. We're gonna convert that range into an iterator and we're gonna step, excuse me, into iter. And we're gonna step by our step that we have to cast to AU size to appease Rust. So here's our one line implementation of Postgres's generate series function. And while that's compiling, we'll look at the definition here. So this is our function. It takes our three arguments. Step has a default of one and it returns a set of big ints. And again, we need to drop and recreate our extension. And now we have my generate series. My generate series, we'll just go one to 10. So there you go. Pretty easy to do that. Whereas the actual C implementation for this function, it's about 70 lines long starts here. Ends down here. There's all this SRF set returning function API that you've got to do correctly in line for is it the first call or per call set up and next and done. And then there's memory context management that you have to do to make sure that the data that you're allocating to store between each run doesn't get freed by Postgres. And all that PGX just takes care of all of that, all of that for us. And so it's real natural to just do this sort of thing with rust. I've got. There's two more things that I'd like to show. If there's questions at this point. I'm happy to. To entertain those. Right. About the null array handling. Generated at runtime. That error was generated at runtime. And the reason. That that error was generated at runtime. Is because Postgres doesn't have a way. Of. Of creating an array that isn't allowed to contain nulls. Right there. You just have an array of values of a particular type where one of those values could one or more of those values could be null. There's no way for Postgres to do that. Let's check the entire array before calling your code. So that's, that's a good question in. Postgres has an internal function that short circuits that they, they, they know kind of a Postgres knows a priori whether or not an array does contain a null. There's just no syntax for saying it can't. So it's, that's a pretty, pretty quick check. There's also. Yeah. So. The, the, the, the, the VEC is, is an owned VEC in rusts. So we can also do a ray. This is a PGX thing of I 64. And it would work a little bit differently, but it would look like this. And so, this is a zero copy sort of thing. Whereas the owned VEC is. Is copy. So. 35 minutes is a short amount of time. I want to show what I think is really neat. I guess what I think is really neat about this is that we haven't written any SQL yet for creating our extension. So let's really do some stuff and also not write any SQL. I've got this sub module called phone number. It defines a really simple type, and I put as little thought into what an actual phone number might need to look like as possible, but it defines a very simple REST type that has two unsigned 16-bit integers for the area code, the exchange, and the actual number. And then there's all these REST-derived macros attached to phone number. And kind of like the comments here say, this gives us a standard REST ability to do comparisons of the type phone number. Our two instances of a phone number equal is one greater than or less than the other. So equality comes from equal and partial equal. It's kind of bad ordering. And then the greater than, less than, less than equal, greater than equal come from ORD and partial ORD. And hash would be for doing hash code comparisons. REST doesn't give you the ability to do these things on a type unless you implement them yourself or in pretty much all cases, you can just have REST derive all that code for you. So the next thing is we can derive a Postgres type. This is something that's part of PGX. Hi, yes. It is possible to execute SQL statements. PGX has support for SPI, and it's actually really, really slick. And if we have time at the end, I'd like to circle back to that. That was one of the last two things that I wanted to show. So yeah, we'll circle back to that. But Postgres type, the derived Postgres type macro in combination with RESTs, this is through the seridecrate, serialize and deserialize, does the needful to be able to represent this type inside Postgres. And then these derived Postgres equals ORD and hash macros are going to tell PGX to generate the code, to generate all the SQL for doing SQL level comparisons of greater than, less than, greater than equal, less than equal, equal, so on and so forth. It also generates the type, or it generates the operator op class and operator families so that that type can then be used not only on disk, but you can create B tree or hash indices over these things too. And from here, the very last thing is we want to, we're telling PGX that we want to write our own textual input output functions, right? Because a phone number might look like, you know, 800-555-1212, right? That's what we'd want a phone number to look like. So, PGX's default is to represent these things as JSON, as text as JSON on disk they're represented in a binary format. But for purposes of this demo, I thought going through the little bit of extra work to actually be able to type out a phone number would be, would be better. So this in outfunks is a trait that from PGX that we're implementing for phone number. And it's just two functions that we have to implement, input and output. And I'll quickly follow into what we're doing here, but we're taking our input, which Postgres gives us that is a C string. And the first thing that we want to do is we want to try to convert that into a valid Rust string, which necessitates that that input string be valid UTF-8. And then if that happens, we want to parse it, if that succeeds, we want to parse it into a phone number. Rust is figuring out that we're trying to parse it into a phone number. The compiler is figuring that out for us. And we want to expect that that phone number is a valid format if it isn't. If either it's not valid UTF-8 input or it isn't a valid phone number, we would want to raise an error in Postgres. And then the output is literally just converting self, which is of type phone number into a string. And pushing it into the string info structure, which is something internal to Postgres. And then there's quick Rust implementation behind the scenes for this. For the outputs function here, where we use this format macro to convert it to a string, that ties to an implementation of display, where we're writing to this formatter the area code with a width of three, the exchange with a width of three, and the number with a zero padded width of four. And then we have our from string implementation for phone number. This is what parses the input phone number, you know, the 800-555-1212 into an actual phone number object. And I'm not going to go through the code here, but this is all just pure, pure Rust. There's a little validator function in line for validating each part of the area code, the exchange, and the number that will return one of our phone number parse error enum variants. And then if it all works, we have a valid results that is our phone number. So, let's, let me show one other thing. I have a, went ahead and wrote another little function here called random phone number. This will just give us a random phone number, right? So, select random phone number, if I could type. Yeah, so we can just keep calling this and get random phone number. So, we could create table, let's call it, let's just call it numbers, as select random phone number, we'll call it PN, as PN from, and we'll use our generate series function. Let's do 100,000 of those. I'll do that, right? Yeah, so we have 100,000 random phone numbers here in our, in our numbers table. And let's say we want to, we want to search for one of those, right? So, select star from numbers where PN equals, equals that. So, that's what we'll do it a couple times, 500, almost 600 milliseconds, five, yeah. And Postgres has planned a sequential scan on this, because we don't have an index on it. So, let's create an index, we'll just call it numbers PN on numbers PN. So, now we've created a B tree index on that table, or we are creating a B tree index on that table, he says. And now, if we do an explain, well, if we just run it, and now it's, it's pretty much instantaneous, we're sub, sub one millisecond, probably on average here. And this is all of our Rust code compiled and debug. And so now Postgres is planning a bitmap index scan. And I think we could even get it to, to plan. Yeah, well, okay. So, we wrote no SQL. Really, all we did was define a Rust type, and apply a bunch of drive macros to it, and a little bit of Rust code to make, to make this type easy to deal with. And PGX generated all this code for us to generate our input and output functions. Well, to generate, to create the type, it's input and output functions to finalize the type, our random phone number function that we did write. And then all of our operators for equals, not equals, you know, less than, greater than, less than equal with proper negator, commutator declarations all the way through. And then it generates the operator class and families for, for both hash and, and B tree. So there's a lot of, a lot of power that PGX brings us in, in writing a little bit of code, and having a lot of flexibility in SQL and also ensuring that all this stuff is actually correct, right? It's all machine generated. So assuming there's no bugs in, sorry, assuming there's no bugs in PGX, then, you know, your SQL is going to be correct for, for these sorts of things as well. What's hash used for? There's a index type called hash that is an on, on disk hash table. It doesn't, it only does equality testing or, you know, set containment, containment testing, it can't do range queries and stuff. It may or may not be faster in certain situations. But that's a lot to, to cover real quickly, isn't it? Again, with, with the phone numbers here, this is just a contrived example, but you get a lot of bang for your buck on, on this, this sort of stuff with, with, with PGX. There was a question that someone asked a little bit ago about SQL statements. Sure. Well, and let's, let's, let's do this even. Let's tie this together. Let's make a PGX turn function, exchange part of the phone number. World's DB, there's things that we could do to, to just index the exchange or, or rather index the full number, but you know, like do range queries to, to get us between two exchanges. You know, and I think that would work. Let's say we're greater than 600, greater than equal. I don't know what date is in here, right? It's all random. And PN is less than equal to, well, less than 700. Just, I don't know how, what it's going to pull back. I'm still in a sequential scan, but yeah, we, we can, we can do that. So I don't know off the top of my head why it decided to do a sequential scan there, but anyway, let me show real quick SPI and we'll use phone number with this. So let's say, let's do this prefix of a, we'll make it a string index part of the phone number with separate expression index. Yeah, you could do that too. Let's just return a vec of phone number. So maybe the way that we would do this. I don't have, I'm making this up off the top of my head here. So let's just use this range example, start and end or prefix. So we could, let's see, let's make an SQL statement. Yeah. So there you go. Good, good call. I didn't analyze the database. So, so yeah, that's, that's using the index there too. Good call worlds DB. So let's do array AG of our phone number from numbers where PN is greater than equal to our prefix 000 and PN is less than our, our ending prefix. And please pardon the, the SQL injection opportunities here. We're just, we're just doing this real quickly. Okay. So I'll just note that, that they exist and you guys cannot figure that out for yourselves later. Okay. So we've made a little SQL statement to do, to do this, right? But it's going to, going to array AG this and with, without the limit. So what do we do next? We call SPI and the easiest way, we tried to make SPI really easy to use with Russ because it's incredibly difficult and finicky to use with, with C, especially because you have to deal with Cs or Postgres's memory contexts and returning datums out of the SPI session. So got this function called get one and it's literally um, we can just pass in a reference to our, our string and then we just have to assert that it isn't null. So we would expect this to be um, uh, SPI result was no, I don't know why it ever would be in this situation, but I guess it could be. And so what this is going to do is it's going to execute our SQL statement, assuming we typed it in, right? And get one means get the first, get the first field being returned that is an array of phone numbers and PGX will automatically convert that into a VECA phone numbers and ship that back to, back to Postgres. So um, if we compile and rerun our, yeah, thanks Lindsay. Um, I think we'll can end writing code right about here in just a minute. We need to drop and, drop and recreate our extension. Um, create extension Postgres cop. Oh, cool. Uh, we have to cascade drop our extension because we have a table with the data type, with the column whose data type depends on it. Uh, what's, what's happening here? Since you're out of here. Oh, you got a spell it right too. So create extension Postgres cop and let's, we got to recreate our table here table numbers. So create table numbers as selects. Okay, sorry. Um, and then let's recreate our index again on numbers. Okay, so PGX have an option to not create the get repo. I'm going to convert it into PGX and existing projects already and get, um, that's a great question. I, it does not, but I would happily accept a request for that and I can understand why that might be a little frustrating. Um, okay. Sorry friends. Let's now call our, our SPI example function and we'll start with 600 and go through 700. Let's do that again. Example, we'll start at 600 and go through 700 and this time, this time I mean it. So this is our, our SPI usage example here and you can, uh, there's a lot more advanced things that you can do with SPI, but here's just a quick, quick off the cuff example of what SPI might look like. Um, I actually ended up skipping quite a number of other things that I wanted to show. Um, but I have pushed to our, to ZomboDB's GitHub repo and that would be, um, type this out, github.com slash ZomboDB. There is a, a Postgres comp repository there that contains everything that I intended to, to talk through today, including the phone number. Um, it doesn't have this little SPI example there, but I'll, I'll add that to it. I didn't get a chance to talk about unit testing, which is, which is super cool, but we're kind of, we're kind of at the end of the, the clock here, but I'm happy to entertain more questions if there's some other example things that I could write. Um, I'd be happy to do that. It's a lot to take in to, to be like, like, if you've never written Postgres extensions before and you've never written Rust before, um, it's, it's a lot to take in. Um, and if, if you've done one or the other, I think maybe it starts to make sense. We wanted to, to try to make PGX as natural to Rust programmers as it could be and, and require as little knowledge of Postgres internals as, as possible. Um, but at the same time, PGX is, is also provides full, unsafe access to the entirety of Postgres as internals and, and not to suggest that what I'm about to say is a good idea or a bad idea or that these companies should do this. PGX is capable of, of implementing or rather, yeah, PGX is capable of, of having something like Citus or Timescale, implemented using PGX. So I mean, it's, it's pretty much fully featured. Um, the idea for today's presentation was just to talk, show a little bit of easy things to maybe get your, get your brain juices flowing and thinking of ideas for things you might want to do with PGX and Rust yourself in the future. So Postgres 13 support plan. Yes, it is. Um, we are trying to wrap up some, some work with, uh, on our other project, which is ZomboDB. And as soon as that's done, we're going to hit Postgres 13 support. It'll be fairly easy, but it'll probably take, take a couple of weeks to get that done. And for, for what it's worth, this is what myself and I have one employee, David, who I think is, is listening today. Um, this is what we do full-time. This is not, this is not some side project that, that we do for fun on the weekends. This is, this is our full-time gig. So this, this stuff pays our bills and we intend to keep it, keep it open source and available. Um, so any other questions or anything? One more question from WorldsDB. This might be off topic, uh, but what's the funding model? Yeah, which is, um, I'm not going to get too deep into that because that's not what this webinar is about. The funding model is to be able to buy my giant St. Bernard, lots of dog food, um, afford to buy the, the funding model in general for PGX is, is GitHub sponsorships. If anyone here wants to go sponsor on GitHub, we would sincerely appreciate that, but I'm not here to promote that today. Outside of that, um, you know, we sell consulting services and, uh, software support agreements on top of ZomboDB and what also, also entertained doing that for, uh, you know, professional consulting services for PGX development that you might need to do in-house with, with your company. And as far as considered wrapping up the GIST and GIN APIs, yeah, I've considered it. Um, those, those are eminently doable. Um, I need to study them a little bit more. I've never implemented them myself. Um, but yeah, GIN and GIST index, uh, API supported me nice to, to get done. Like I said, we've, we spent a year on, on PGX and we're about to, to drop a new version of ZomboDB that's been fully rewritten to PGX. And I think that's the best decision I've ever made in, in software development so far. But I'm out of words, Lindsay. Yeah, you can always tell a fantastic webinar by the amount of engagement and gosh, was this an engaging webinar. Um, so wonderful. Thank you so much for your time, Eric. Um, thank you to all of our, uh, attendees. Thank you for spending a little bit of your day with us. Um, so whether it's morning, afternoon or evening, I hope you have a great one and hopefully I'll see you back here tomorrow for our next webinar. So cheers and thank you once again. Cool. Thanks. Have a good day, everybody.