<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://illuminatedcomputing.com/">
  <id>https://illuminatedcomputing.com/</id>
  <title>Illuminated Computing</title>
  <updated>2024-10-23T00:00:00Z</updated>
  <link rel="alternate" href="https://illuminatedcomputing.com/" type="text/html"/>
  <link rel="self" href="https://illuminatedcomputing.com/tags/postgres/atom.xml" type="application/atom+xml"/>
  <author>
    <name>Paul A. Jungwirth</name>
    <uri>https://illuminatedcommputing.com/</uri>
  </author>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-10-23:/posts/2024/10/bison-shift-reduce-conflict/</id>
    <title type="html">Solving bison shift/reduce conflicts in Postgres</title>
    <published>2024-10-23T00:00:00Z</published>
    <updated>2024-10-23T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/10/bison-shift-reduce-conflict/" type="text/html"/>
    <content type="html">
&lt;p&gt;I had to fix some shift/reduce conflicts in the Postgres bison grammar recently.&lt;/p&gt;

&lt;p&gt;I’ve never done this before, so it was a learning experience. Maybe my story will help some other new Postgres contributor—or anyone struggling with this technology that is central to computer science but for many day-to-day programmers seldom-used.&lt;/p&gt;

&lt;p&gt;Back in 2018 I read &lt;em&gt;lex &amp;amp; yacc&lt;/em&gt; by Doug Brown, John R. Levine, and Tony Mason (second edition published in 1992). Levine’s 2009 &lt;em&gt;flex &amp;amp; bison&lt;/em&gt; would have been a more practical choice, but I liked getting some history too. Re-reading some parts of that book was very helpful. So were &lt;a href="https://www.gnu.org/software/bison/manual/html_node/Algorithm.html"&gt;the bison manual&lt;/a&gt; and &lt;a href="https://stackoverflow.com/questions/26188276/why-doesnt-prec-have-an-effect-in-this-bison-grammar?noredirect=1&amp;amp;lq=1"&gt;some&lt;/a&gt; &lt;a href="https://stackoverflow.com/questions/9716917/why-does-this-simple-grammar-have-a-shift-reduce-conflict?rq=3"&gt;StackOverflow&lt;/a&gt; &lt;a href="https://stackoverflow.com/questions/76244745/bison-nonassoc-vs-token"&gt;questions&lt;/a&gt;. Now I’m working through &lt;a href="https://en.wikipedia.org/wiki/Compilers:_Principles,_Techniques,_and_Tools"&gt;The Dragon Book&lt;/a&gt;, and that would have made a great resource too. It’s easy to write some bison without going that deep, but if you get stuck it can be frustrating.&lt;/p&gt;

&lt;p&gt;I’ve been adding syntax from SQL:2011 for &lt;a href="https://sigmodrecord.org/publications/sigmodRecord/1209/pdfs/07.industry.kulkarni.pdf"&gt;application-time updates and deletes&lt;/a&gt;. If you have a &lt;code&gt;PERIOD&lt;/code&gt; or range column named &lt;code&gt;valid_at&lt;/code&gt;, you can say &lt;code&gt;UPDATE t FOR PORTION OF valid_at FROM '2024-01-01' TO '2024-02-01' SET foo = bar&lt;/code&gt;. (For more details you can &lt;a href="/pages/temporal-data-theory-and-postgres/"&gt;watch my talk&lt;/a&gt;.) The &lt;code&gt;FOR PORTION OF&lt;/code&gt; bounds don’t have to be just literals. You could also say, for example, &lt;code&gt;FROM current_time TO current_time + INTERVAL '1' HOUR&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Actually I picked that example on purpose. Did you know that intervals support this syntax: &lt;code&gt;INTERVAL '1:02:03' HOUR TO MINUTE&lt;/code&gt;? That means: Interpret the string as hours, and preserve the precision down to the minute. By default if you ask for &lt;code&gt;1:02:03 HOUR&lt;/code&gt; you get just an hour. But &lt;code&gt;TO MINUTE&lt;/code&gt; means you get 1 hour and 2 minutes. (You still lose the seconds.)&lt;/p&gt;

&lt;p&gt;So how does &lt;a href="https://en.wikipedia.org/wiki/LR_parser"&gt;an LR(1) parser&lt;/a&gt; deal with &lt;code&gt;FOR PORTION OF valid_at FROM current_time + INTERVAL '1' HOUR TO MINUTE&lt;/code&gt;? When we consider the &lt;code&gt;TO&lt;/code&gt;, does it belong with the interval, or does it start the closing bound of the &lt;code&gt;FOR PORTION OF&lt;/code&gt;? This is a shift/reduce conflict. Bison can’t look ahead further than one token to guess what is correct—and I’m not sure it would help even if it could.&lt;/p&gt;

&lt;p&gt;When the next token is able to complete some piece of the grammar, called a “rule”, the parser “reduces”: it consumes those tokens and lets you run a custom “action” attached to your rule. Otherwise, bison shifts the token onto a stack of not-yet-reduced tokens, so that it can reduce a rule in the future. But bison needs to decide each time it sees a token whether to reduce or to shift.&lt;/p&gt;

&lt;p&gt;Here is the rule for &lt;code&gt;FOR PORTION OF&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;for_portion_of_clause:
  FOR PORTION OF ColId FROM a_expr TO a_expr
    {
      ForPortionOfClause *n = makeNode(ForPortionOfClause);
      n-&amp;gt;range_name = $4;
      n-&amp;gt;location = @4;
      n-&amp;gt;target_start = $6;
      n-&amp;gt;target_end = $8;
      $$ = n;
    }
;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The first line is the name of the rule, so we can use it in higher-level contexts: the &lt;code&gt;UPDATE&lt;/code&gt; statement (and also &lt;code&gt;DELETE&lt;/code&gt;). The second line has the inputs we need to match to complete the rule. Some of them are “terminals”: keywords, identifiers, operators, literals, punctuation, etc.—in this case &lt;code&gt;FOR&lt;/code&gt;, &lt;code&gt;PORTION&lt;/code&gt;, &lt;code&gt;OF,&lt;/code&gt; &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;TO&lt;/code&gt;. Some are “non-terminal”: further rules. Below all that is a block of C code: the action that gets run when we reduce. We call the name the “left side” of the rule, and the inputs the “right side” or “body”. Rules are also sometimes called “productions”. The &lt;em&gt;lex &amp;amp; yacc&lt;/em&gt; book doesn’t use that terminology, but the Dragon Book does, and so does the Postgres source code.&lt;/p&gt;

&lt;p&gt;So each bound is an &lt;code&gt;a_expr&lt;/code&gt;. The &lt;code&gt;a_expr&lt;/code&gt; rule is a complicated production with just about anything you can do in Postgres: a literal, a variable, a function call, an operator, a subquery, lots of weird SQL keywords, etc. Many contexts forbid some of these things, e.g. you can’t refer to a column in a &lt;code&gt;DEFAULT&lt;/code&gt; expression or a partition bound—but that is enforced during analysis, not by the grammar.&lt;/p&gt;

&lt;p&gt;To speak more precisely, when a non-terminal can match inputs in more than one way (like &lt;code&gt;a_expr&lt;/code&gt;), we should call &lt;em&gt;each alternative&lt;/em&gt; a rule or production. But in bison you commonly write the name once then separate each body with a pipe (&lt;code&gt;|&lt;/code&gt;), so all the rules share one name. There is not one &lt;code&gt;a_expr&lt;/code&gt; rule, but many: 68 by my count. But such terminological precision is rarely needed.&lt;/p&gt;

&lt;p&gt;Take our example, &lt;code&gt;FOR PORTION OF valid_at FROM current_time + INTERVAL '1' HOUR • TO MINUTE&lt;/code&gt;. I’ve added a dot to represent bison’s “cursor”. It is considering what to do with the &lt;code&gt;TO&lt;/code&gt;. We could reduce the &lt;code&gt;a_expr&lt;/code&gt; right now, leaving the &lt;code&gt;TO&lt;/code&gt; to become part of the &lt;code&gt;FOR PORTION OF&lt;/code&gt;. Or we could shift the &lt;code&gt;TO&lt;/code&gt; so that it eventually gets reduced as part of the &lt;code&gt;a_expr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Actually it’s not about reducing the &lt;code&gt;a_expr&lt;/code&gt;, but reducing one of its many sub-rules, in this case the interval. An &lt;code&gt;a_expr&lt;/code&gt; can be a &lt;code&gt;c_expr&lt;/code&gt; (among other things), and a &lt;code&gt;c_expr&lt;/code&gt; can be an &lt;code&gt;AexprConst&lt;/code&gt; (among other things), and an &lt;code&gt;AexprConst&lt;/code&gt; can be a &lt;code&gt;ConstInterval Sconst opt_interval&lt;/code&gt; (among other things), and the &lt;code&gt;opt_interval&lt;/code&gt; is the problem, because it can optionally have a &lt;code&gt;TO&lt;/code&gt;. Here is that rule:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;opt_interval:
  YEAR_P
    { $$ = list_make1(makeIntConst(INTERVAL_MASK(YEAR), @1)); }
  | MONTH_P
    { $$ = list_make1(makeIntConst(INTERVAL_MASK(MONTH), @1)); }
  | DAY_P
    { $$ = list_make1(makeIntConst(INTERVAL_MASK(DAY), @1)); }
  | HOUR_P
    { $$ = list_make1(makeIntConst(INTERVAL_MASK(HOUR), @1)); }
  | MINUTE_P
    { $$ = list_make1(makeIntConst(INTERVAL_MASK(MINUTE), @1)); }
  | interval_second
    { $$ = $1; }
  | YEAR_P TO MONTH_P
    { ... }
  | DAY_P TO HOUR_P
    { ... }
  | DAY_P TO MINUTE_P
    { ... }
  | DAY_P TO interval_second
    { ... }
  | HOUR_P TO MINUTE_P
    { ... }
  | HOUR_P TO interval_second
    { ... }
  | MINUTE_P TO interval_second
    { ... }
  | /*EMPTY*/
    { $$ = NIL; }
;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;(I’ve omitted most of the actions, since actions don’t affect bison’s choices.) The &lt;code&gt;opt_interval&lt;/code&gt; rule is what bison is trying to reduce.&lt;/p&gt;

&lt;p&gt;When you have a shift/reduce conflict, &lt;code&gt;make&lt;/code&gt; gives you an error like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/usr/bin/bison -d -o gram.c gram.y
gram.y: conflicts: 4 shift/reduce
gram.y: expected 0 shift/reduce conflicts
make[2]: *** [gram.c] Error 1
make[1]: *** [parser/gram.h] Error 2
make: *** [submake-generated-headers] Error 2&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That is not much to go on.&lt;/p&gt;

&lt;p&gt;By the way, do you see the &lt;code&gt;expected 0&lt;/code&gt;? A shift/reduce conflict doesn’t have to be fatal. Bison will default to shift. But this is a bit sketchy. It means you could accidentally write an ambiguous grammar that causes trouble later. So bison lets you declare how many conflicts you expect, and it only fails if it finds a different count. I like that for Postgres the expected conflict count is zero. For MariaDB &lt;a href="https://github.com/MariaDB/server/blob/4b6922a315fa5411665ac99c0b40fd7238093403/sql/sql_yacc.yy#L357-L361"&gt;it is 62&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Anyway, we have four shift/reduce conflicts. Now what? Let’s ask Bison where they are. It can take a &lt;code&gt;-v/--verbose&lt;/code&gt; option to generate a “report file”. Since Postgres’s grammar lives in &lt;code&gt;gram.y&lt;/code&gt;, the report file is &lt;code&gt;gram.output&lt;/code&gt;. (Modern versions offer more control with &lt;code&gt;-r/--report&lt;/code&gt; and &lt;code&gt;--report-file&lt;/code&gt;, but macOS only supports &lt;code&gt;-v&lt;/code&gt;.) We aren’t running bison directly, but we can control things like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;make BISONFLAGS=-v&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That gives us a file with 5.5 million lines, but right at the top we see:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State 1454 conflicts: 1 shift/reduce
State 1455 conflicts: 1 shift/reduce
State 1456 conflicts: 1 shift/reduce
State 1459 conflicts: 1 shift/reduce&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then if we &lt;code&gt;/^state 1454&lt;/code&gt; we see this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;state 1454

  2000 opt_interval: DAY_P .
  2005             | DAY_P . TO HOUR_P
  2006             | DAY_P . TO MINUTE_P
  2007             | DAY_P . TO interval_second

    TO  shift, and go to state 2670

    TO        [reduce using rule 2000 (opt_interval)]
    $default  reduce using rule 2000 (opt_interval)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So in this state, bison has four candidate rules it could eventually reduce, numbered 2000, 2005, 2006, 2007. Below that are possible valid tokens and what to do for each one. We see &lt;code&gt;TO&lt;/code&gt; twice, which is the problem. The square brackets highlight the conflict: they mark a transition that will never happen. (The &lt;code&gt;$default&lt;/code&gt; line means if the next token is &lt;em&gt;not&lt;/em&gt; a &lt;code&gt;TO&lt;/code&gt;, we can reduce and leave that token for some higher-level rule to match.) So this is how we know one half of the problem is &lt;code&gt;opt_interval&lt;/code&gt;. The other half is &lt;code&gt;for_portion_of_clause&lt;/code&gt;. Bison doesn’t tell us that, but (1) we just added it to a previously-working grammar (2) we can see that &lt;code&gt;TO&lt;/code&gt; is the issue, and that’s where we match a &lt;code&gt;TO&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is one of the four shift/reduce conflicts. The other three are also from &lt;code&gt;opt_interval&lt;/code&gt;, caused by &lt;code&gt;YEAR_P TO MONTH_P&lt;/code&gt;, &lt;code&gt;HOUR_P TO {MINUTE_P,interval_second}&lt;/code&gt;, and &lt;code&gt;MINUTE_P TO interval_second&lt;/code&gt;. Essentially it’s all one conflict, but we can hit the &lt;code&gt;TO&lt;/code&gt; after &lt;code&gt;DAY&lt;/code&gt;, &lt;code&gt;HOUR&lt;/code&gt;, &lt;code&gt;YEAR&lt;/code&gt;, or &lt;code&gt;MINUTE&lt;/code&gt;, so that’s four different states.&lt;/p&gt;

&lt;p&gt;We can use “precedence” to resolve such ambiguities. It’s just like elementary arithmetic: multiplication has higher precedence than addition. It is stickier. We do it first. But what does that mean in bison? Bison compares the precedence of the non-terminal rule it could reduce (&lt;code&gt;opt_interval&lt;/code&gt;) vs the precedence of the token it could shift (&lt;code&gt;TO&lt;/code&gt;). Rules don’t really have precedence themselves, but they get the precedence of their final terminal token.&lt;/p&gt;

&lt;p&gt;So in state 1454, if we give &lt;code&gt;DAY_P&lt;/code&gt; a different precedence than &lt;code&gt;TO&lt;/code&gt;, bison will know whether to reduce (rule 2000), or shift (and eventually reduce rule 2005, 2006, or 2007). If &lt;code&gt;DAY_P&lt;/code&gt; is higher, we’ll reduce. If &lt;code&gt;TO&lt;/code&gt; is higher, we’ll shift.&lt;/p&gt;

&lt;p&gt;Should we shift or reduce? The only answer is to shift. If we reduce by default, then users can &lt;em&gt;never&lt;/em&gt; say &lt;code&gt;INTERVAL '1' DAY TO HOUR&lt;/code&gt; (even in a completely different context). No amount of parens will make bison do otherwise. But if we shift, then this is a syntax error: &lt;code&gt;FOR PORTION OF valid_at FROM '2013-03-01'::timestamp + INTERVAL '1' HOUR TO '2014-01-01'&lt;/code&gt; (because after shifting the &lt;code&gt;TO&lt;/code&gt; bison is still trying to reduce &lt;code&gt;opt_interval&lt;/code&gt;), but this fixes it: &lt;code&gt;FOR PORTION OF valid_at FROM ('2013-03-01'::timestamp + INTERVAL '1' HOUR) TO '2014-01-01'&lt;/code&gt;. Users can get what they want by adding parens.&lt;/p&gt;

&lt;p&gt;So to shift, we give &lt;code&gt;TO&lt;/code&gt; a higher precedence than &lt;code&gt;YEAR_P&lt;/code&gt;, &lt;code&gt;DAY_P&lt;/code&gt;, &lt;code&gt;HOUR_P&lt;/code&gt;, and &lt;code&gt;MINUTE_P&lt;/code&gt;. By default a token has no precedence, but bison lets you make a list of declarations where &lt;em&gt;lower lines&lt;/em&gt; have &lt;em&gt;higher precedence&lt;/em&gt;. So for a long time my patch added this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;%nonassoc YEAR_P DAY_P HOUR_P MINUTE_P
%nonassoc TO&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;(Actually I had &lt;code&gt;MONTH_P&lt;/code&gt; in there too, but that isn’t needed because you can’t have &lt;code&gt;MONTH TO ...&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;But this is frowned upon. There is a comment right above my change that gave me a guilty conscience for at least a year, maybe a few:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/*
 * Sometimes it is necessary to assign precedence to keywords that are not
 * really part of the operator hierarchy, in order to resolve grammar
 * ambiguities.  It's best to avoid doing so whenever possible, because such
 * assignments have global effect and may hide ambiguities besides the one
 * you intended to solve.  (Attaching a precedence to a single rule with
 * %prec is far safer and should be preferred.)  If you must give precedence
 * to a new keyword, try very hard to give it the same precedence as IDENT.
 * If the keyword has IDENT's precedence then it clearly acts the same as
 * non-keywords and other similar keywords, thus reducing the risk of
 * unexpected precedence effects.
 */&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I knew I had to fix this before my patch would get accepted. Those two lines had to go.&lt;/p&gt;

&lt;p&gt;What is the &lt;code&gt;%prec&lt;/code&gt; approach suggested by the comment? I said above that a rule’s precedence comes from its last terminal token. But you can override a rule’s precedence by putting &lt;code&gt;%prec token_name&lt;/code&gt; after the right side. For example &lt;code&gt;a_expr&lt;/code&gt; has this rule:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;| a_expr AT TIME ZONE a_expr      %prec AT
  { ... }&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We’re saying we should reduce this rule with the precedence of &lt;code&gt;AT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I tried all kinds of &lt;code&gt;%prec&lt;/code&gt; placements that didn’t work. My mental model of bison’s process was too vague. The reason I’m writing this story is really to record the details and thought process that finally gave me the solution.&lt;/p&gt;

&lt;p&gt;For example, putting &lt;code&gt;%prec&lt;/code&gt; on &lt;code&gt;for_portion_of_clause&lt;/code&gt; doesn’t do any good, because the conflict is lower down than that, inside &lt;code&gt;opt_interval&lt;/code&gt;. That was counter-intuitive, because I knew that adding &lt;code&gt;for_portion_of_clause&lt;/code&gt; was what caused the problem. It’s what offers bison a way to reduce early and still have a place to use the &lt;code&gt;TO&lt;/code&gt;. But despite &lt;code&gt;for_portion_of_clause&lt;/code&gt; exerting influence, at the moment of decision we are in the middle of a different rule. It’s action-at-a-distance.&lt;/p&gt;

&lt;p&gt;Another breakthrough was realizing that the comparison is between a &lt;em&gt;rule&lt;/em&gt; (to reduce) and a &lt;em&gt;token&lt;/em&gt; (to shift). Within &lt;code&gt;opt_interval&lt;/code&gt; I kept trying to give low precedence to the rules without &lt;code&gt;TO&lt;/code&gt; and high precedence to the rules with it. But the comparison isn’t between two rules. It’s between a rule and a token. The token is &lt;code&gt;TO&lt;/code&gt; itself. There isn’t any way to give precedence to a &lt;em&gt;token&lt;/em&gt; with &lt;code&gt;%prec&lt;/code&gt;. That only modifies a rule. If &lt;code&gt;TO&lt;/code&gt; has an undefined precedence, there will always be a conflict. So I &lt;em&gt;did&lt;/em&gt; have to declare a precedence for &lt;code&gt;TO&lt;/code&gt;, but following the comment above I could give it the same precedence as &lt;code&gt;IDENT&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;%nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
      SET KEYS OBJECT_P SCALAR TO VALUE_P WITH WITHOUT PATH&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then the conflicting &lt;code&gt;opt_interval&lt;/code&gt; rules needed a lower precedence, to prevent reducing early. A low-precedence keyword we use a lot is &lt;code&gt;IS&lt;/code&gt;, so I did this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;opt_interval:
  YEAR_P                                %prec IS
    { ... }
  | DAY_P                               %prec IS
    { ... }
  | HOUR_P                              %prec IS
    { ... }
  | MINUTE_P                            %prec IS
    { ... }&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we’ll shift the &lt;code&gt;TO&lt;/code&gt; and follow those rules that include it.&lt;/p&gt;

&lt;p&gt;Finally I had a solution!&lt;/p&gt;

&lt;p&gt;It’s worth considering another approach. We can also enforce precedence with the structure of our rules, without declaring an explicit higher/lower precedence for terminals. For example for simple arithmetic we could do this (from the Dragon Book, p. 49–50):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;expr: expr + term
  | expr - term
  | term

term: term * factor
  | term / factor
  | factor

factor: digit
  | '(' expr ')'&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For &lt;code&gt;n&lt;/code&gt; levels of precedence, we need &lt;code&gt;n+1&lt;/code&gt; different rules. But I think this approach gets unwieldy quickly. And anyway was I going to rewrite the Postgres grammar to do this?&lt;/p&gt;

&lt;p&gt;Postgres actually does this a bit already though. We’ve seen &lt;code&gt;a_expr&lt;/code&gt; and &lt;code&gt;c_expr&lt;/code&gt;. Of course there is also &lt;code&gt;b_expr&lt;/code&gt;. &lt;code&gt;b_expr&lt;/code&gt; is a more limited set of rules than &lt;code&gt;a_expr&lt;/code&gt;, and &lt;code&gt;c_expr&lt;/code&gt; is everything they have in common.&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;b_expr&lt;/code&gt; to solve some shift/reduce conflicts. For example a column’s &lt;code&gt;DEFAULT&lt;/code&gt; value can only take a &lt;code&gt;b_expr&lt;/code&gt;, because a &lt;code&gt;NOT&lt;/code&gt; would be a shift/reduce conflict: is it a &lt;code&gt;NOT NULL&lt;/code&gt; constraint on the column, or is it part of the &lt;code&gt;DEFAULT&lt;/code&gt; expression, e.g. &lt;code&gt;NOT LIKE&lt;/code&gt;? One rule that &lt;code&gt;b_expr&lt;/code&gt; accepts is &lt;code&gt;'(' a_expr ')'&lt;/code&gt;, so even in contexts like &lt;code&gt;DEFAULT&lt;/code&gt;, you can get whatever you want by wrapping your text in parentheses.&lt;/p&gt;

&lt;p&gt;So could I have saved myself a lot of trouble and made &lt;code&gt;FOR PORTION OF&lt;/code&gt; take &lt;code&gt;FROM b_expr TO b_expr&lt;/code&gt; instead? No, because the problem was inside &lt;code&gt;c_expr&lt;/code&gt;, which is shared by both rules.&lt;/p&gt;

&lt;p&gt;I probably could have invented a &lt;code&gt;d_expr&lt;/code&gt;, but that would have been a lot of work, only to produce a more tangled grammar that I expect no reviewer would have accepted.&lt;/p&gt;

&lt;p&gt;So that’s the story of how I fixed my four shift-reduce conflicts.&lt;/p&gt;

&lt;p&gt;But just when you think you’ve killed the zombie, he rises from the dead. Right around the same time, I realized my grammar was wrong. In SQL, you can give your table an alias when you &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt;. It can use &lt;code&gt;AS&lt;/code&gt; or not: &lt;code&gt;UPDATE tablename [[AS] t]&lt;/code&gt; and &lt;code&gt;DELETE FROM tablename [[AS] t]&lt;/code&gt;. I was putting &lt;code&gt;FOR PORTION OF&lt;/code&gt; &lt;em&gt;after&lt;/em&gt; the alias, but according to SQL:2011 it comes &lt;em&gt;before&lt;/em&gt;. I tried moving it, and I got . . . 30 shift/reduce conflicts!&lt;/p&gt;

&lt;p&gt;These looked really hairy: the problem was that &lt;code&gt;AS&lt;/code&gt; is optional and the alias can be nearly anything. It can’t be a &lt;em&gt;reserved&lt;/em&gt; keyword (unless you quote it), but many keywords are not reserved (per the standard), so there’s ambiguity there. Allowing &lt;code&gt;a_expr&lt;/code&gt;, which can be nearly anything, followed by an optional alias, which can be nearly anything, is bad news. I really thought I was in trouble.&lt;/p&gt;

&lt;p&gt;Could I just ignore the standard? I don’t think that would be acceptable, not in this matter. But it was tempting enough that I checked what MariaDB and IBM DB2 were doing. Somehow they were making it work. I should figure it out too.&lt;/p&gt;

&lt;p&gt;I think I took a walk, or maybe I slept on it, but I realized that we already have the same problem with &lt;em&gt;column&lt;/em&gt; aliases when you &lt;code&gt;SELECT&lt;/code&gt;. Each selected column is an &lt;code&gt;a_expr&lt;/code&gt;, and their aliases don’t require &lt;code&gt;AS&lt;/code&gt;. What was Postgres doing to make that work?&lt;/p&gt;

&lt;p&gt;I found this rule for &lt;code&gt;SELECT&lt;/code&gt;ing:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;target_el:  a_expr AS ColLabel { ...}
      | a_expr BareColLabel { ... }
      | a_expr { ... }
      | '*' { ... }
    ;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It turns out that &lt;code&gt;ColLabel&lt;/code&gt; allows anything (even reserved keywords!), but &lt;code&gt;BareColLabel&lt;/code&gt; is more restricted.&lt;/p&gt;

&lt;p&gt;So I could do something similar: when there is an &lt;code&gt;AS&lt;/code&gt;, permit everything, but otherwise only permit tokens that are conflict-free. If fact to keep backward-compatibility, I could leave the old grammar rule for &lt;code&gt;UPDATE&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; in place (each had only one), and only get more restrictive when &lt;code&gt;FOR PORTION OF&lt;/code&gt; is present. Maybe reviewers will ask me to change things, but at the moment my solution looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;opt_alias:
  AS ColId { ... }
  | BareColLabel { ... }
  | /* empty */ %prec UMINUS { $$ = NULL; }
;

UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
  SET set_clause_list
  from_clause
  where_or_current_clause
  returning_clause
    { ... }
  | opt_with_clause UPDATE relation_expr
  for_portion_of_clause opt_alias
  SET set_clause_list
  from_clause
  where_or_current_clause
  returning_clause
    { ... }
;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I’m not sure I like using &lt;code&gt;BareColLabel&lt;/code&gt; for non-column aliases, but the existing &lt;code&gt;relation_expr_opt_alias&lt;/code&gt; uses &lt;code&gt;ColId&lt;/code&gt;, so maybe it’s okay.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;%prec&lt;/code&gt; is necessary to resolve a conflict with &lt;code&gt;USING&lt;/code&gt;, which is permitted by &lt;code&gt;BareColLabel&lt;/code&gt;, and also allowed in &lt;code&gt;DELETE FROM ... USING&lt;/code&gt;. If I added a separate list for bare &lt;em&gt;table&lt;/em&gt; labels, we could leave out &lt;code&gt;USING&lt;/code&gt; and not use &lt;code&gt;%prec&lt;/code&gt; here, but I don’t think maintaining another keyword list would be popular.&lt;/p&gt;

&lt;p&gt;That’s it! I’m happy that at 47 I can still work out the errors in my mental model of something and correct them. Hopefully by writing this down I won’t have to do it more than once. :-)&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-10-13:/posts/2024/10/postgres-replica-identity/</id>
    <title type="html">Postgres REPLICA IDENTITY</title>
    <published>2024-10-13T00:00:00Z</published>
    <updated>2024-10-13T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/10/postgres-replica-identity/" type="text/html"/>
    <content type="html">
&lt;p&gt;Both logical decoding and logical replication use a table’s &lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-REPLICA-IDENTITY"&gt;&lt;code&gt;REPLICA IDENTITY&lt;/code&gt;&lt;/a&gt;. This is a way to say which row was changed by an &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt;. In other word it identifies the “old” row.&lt;/p&gt;

&lt;p&gt;Logical &lt;em&gt;decoding&lt;/em&gt; will use the replica identity to say which row was changed, if available, but if not then the information is simply omitted. No big deal.&lt;/p&gt;

&lt;p&gt;In logical &lt;em&gt;replication&lt;/em&gt;, the subscriber looks for the replica identity with each change, and it uses it to know which row to remove, either because it was replaced or because it was deleted. So you can always replicate inserts and truncates, but you can only replicate updates and deletes if you have an appropriate replica identity. In fact even on the publication side, Postgres will forbid changes without an appropriate replica identity, as we will see.&lt;/p&gt;

&lt;p&gt;Normally a table has &lt;code&gt;DEFAULT&lt;/code&gt; for its replica identity. This means it will use the table’s primary key (if present). You can’t set the replica identity when you create the table, but you can change it with &lt;code&gt;ALTER TABLE&lt;/code&gt;. Besides &lt;code&gt;DEFAULT&lt;/code&gt;, it can be &lt;code&gt;USING INDEX &amp;lt;index&amp;gt;&lt;/code&gt; or &lt;code&gt;FULL&lt;/code&gt; or &lt;code&gt;NOTHING&lt;/code&gt;. The replica identity is stored in &lt;code&gt;pg_class.relreplident&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had a lot of questions about how each of these works. Mostly I wanted to know when things failed: creating/altering the table/publication, making the change, or receiving it. All my tests were done on Postgres 17 using the &lt;code&gt;REL_17_0&lt;/code&gt; tag after doing &lt;code&gt;make world &amp;amp;&amp;amp; make install-world&lt;/code&gt;. I wanted to replicate from one database to another in the same cluster, but &lt;a href="https://www.postgresql.org/docs/current/sql-createsubscription.html#SQL-CREATESUBSCRIPTION-NOTES"&gt;that requires some (slightly) more complicated commands&lt;/a&gt;. To keep the SQL simple, I ran separate clusters, one with a database named &lt;code&gt;sender&lt;/code&gt;, the other with a database named &lt;code&gt;receiver&lt;/code&gt;. The sending cluster runs on 5432 to keep the receiver’s connection strings simpler.&lt;/p&gt;

&lt;p&gt;On the sender I set &lt;code&gt;wal_level&lt;/code&gt; like this (on a Mac):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sed -i '' -e '/wal_level/a\
wal_level=logical' ~/local/pgsql/data/postgresql.conf&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;On Linux I think this would be just:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sed -i '/wal_level/awal_level=logical' ~/local/pgsql/data/postgresql.conf&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can find the SQL for each test in &lt;a href="https://github.com/pjungwir/replica-identity-tests"&gt;this github repo&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=""&gt;&lt;code&gt;NOTHING&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Let’s start with &lt;code&gt;NOTHING&lt;/code&gt;. This means there is no information about the old row. If we’re looking for failures, this is nice and simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q1: What happens if you create a publication for it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is allowed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q2: What happens if you add it to an existing publication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is allowed too:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p;
CREATE PUBLICATION
sender=# alter publication p add table t;
ALTER PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q3: What happens if you create a &lt;code&gt;FOR ALL TABLES&lt;/code&gt; publication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And this is allowed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p for all tables;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q4: What happens if you set a table to &lt;code&gt;NOTHING&lt;/code&gt; when it already belongs to a publication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s allowed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# create publication p for table t;
CREATE PUBLICATION
sender=# alter table t replica identity nothing;
ALTER TABLE&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q5: What happens if you set a table to &lt;code&gt;NOTHING&lt;/code&gt; when you already have a &lt;code&gt;FOR ALL TABLES&lt;/code&gt; publication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s allowed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# create publication p for all tables;
CREATE PUBLICATION
sender=# alter table t replica identity nothing;
ALTER TABLE&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q6: What happens if you change an insert-only publication to an update publication, and it has a &lt;code&gt;NOTHING&lt;/code&gt; table?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s allowed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p for table t with (publish = insert);
CREATE PUBLICATION
sender=# alter publication p set (publish = 'insert,update,delete,truncate');
ALTER PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q7: What happens if you change an insert-only &lt;code&gt;FOR ALL TABLES&lt;/code&gt; publication to an update publication, and the database has a &lt;code&gt;NOTHING&lt;/code&gt; table?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s allowed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p for all tables with (publish = insert);
CREATE PUBLICATION
sender=# alter publication p set (publish = 'insert,update,delete,truncate');
ALTER PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q8: What happens if you update a &lt;code&gt;NOTHING&lt;/code&gt; table that is in a publication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s &lt;em&gt;not&lt;/em&gt; allowed!&lt;/p&gt;

&lt;p&gt;This fails on the publisher side, even if there is no subscription:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION
sender=# insert into t values ('a');
INSERT 0 1
sender=# update t set a = 'b';
ERROR:  cannot update table "t" because it does not have a replica identity and publishes updates
HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q9: What happens if you delete from a &lt;code&gt;NOTHING&lt;/code&gt; table that is in a publication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This fails the same way:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION
sender=# insert into t values ('a');
INSERT 0 1
sender=# update t set a = 'b';
ERROR:  cannot delete from table "t" because it does not have a replica identity and publishes deletes
HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So to summarize, Postgres doesn’t validate publications up front, but only when you try to send through them a &lt;code&gt;NOTHING&lt;/code&gt; update/delete.&lt;/p&gt;

&lt;h3 id="_2"&gt;&lt;code&gt;FULL&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FULL&lt;/code&gt; means all columns are combined to determine uniqueness. Of course that might &lt;em&gt;still&lt;/em&gt; not be unique. So what happens if you update one of them but not the other?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q1: What happens if you have two identical records and you delete one?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the publisher:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity full;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;On the subscriber:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;receiver=# create table t (a text);
CREATE TABLE
receiver=# create subscription s connection 'dbname=sender' publication p;
NOTICE:  created replication slot "s" on publisher
CREATE SUBSCRIPTION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Back on the publisher:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# insert into t values ('a');
INSERT 0 1
sender=# insert into t values ('a');
INSERT 0 1
sender=# select ctid, a from t;
 ctid  | a
-------+---
 (0,1) | a
 (0,2) | a
(2 rows)

sender=# delete from t where ctid = '(0,1)';
DELETE 1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And the receiver sees:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;receiver=# select * from t;
 a 
---
 a
(1 row)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So we didn’t lose both rows! I guess that’s because we are replicating a delete of one row. Which one we delete doesn’t matter, but we’ll only delete one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q1: What happens if you have two identical records and you delete both?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the publisher:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity full;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;On the subscriber:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;receiver=# create table t (a text);
CREATE TABLE
receiver=# create subscription s connection 'dbname=sender' publication p;
NOTICE:  created replication slot "s" on publisher
CREATE SUBSCRIPTION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Back on the publisher:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# insert into t values ('a');
INSERT 0 1
sender=# insert into t values ('a');
INSERT 0 1
sender=# delete from t;
DELETE 2&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And the receiver sees:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;receiver=# select * from t;
 a 
---
(0 rows)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So both rows disappeared. That makes sense, because the receiver got two messages to delete a row like &lt;code&gt;('a')&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q3: What happens if you have two identical records and you update one?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I assume updating one of two identical rows will work the same, but let’s check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity full;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;receiver=# create table t (a text);
CREATE TABLE
receiver=# create subscription s connection 'dbname=sender' publication p;
NOTICE:  created replication slot "s" on publisher
CREATE SUBSCRIPTION&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;sender=# insert into t values ('a');
INSERT 0 1
sender=# insert into t values ('a');
INSERT 0 1
sender=# select ctid, a from t;
 ctid  | a
-------+---
 (0,1) | a
 (0,2) | a
(2 rows)&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;sender=# update t set a = 'b' where ctid = '(0,1)';
UPDATE 1&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;receiver=# select * from t;
 a 
---
 a
 b
(2 rows)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yep!&lt;/p&gt;

&lt;p&gt;So with &lt;code&gt;FULL&lt;/code&gt; there are no errors on the publisher side nor on the subscriber side. The disadvantage is that everything is slower: logical decoding sends more data, and the subscriber must compare all the columns for equality.&lt;/p&gt;

&lt;p&gt;I wonder if the subscriber will still use an index to apply changes if there is one? I haven’t tested that yet, but if I do I will put an update here.&lt;/p&gt;

&lt;h3 id="_3"&gt;&lt;code&gt;USING INDEX &amp;lt;index&amp;gt;&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;This lets us choose a &lt;code&gt;UNIQUE&lt;/code&gt; index, as long as none of its keys are nullable. Assuming the subscriber has the same uniqueness, it can quickly locate which rows to change. Here is the happy path:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text not null unique);
CREATE TABLE
sender=# alter table t replica identity using index t_a_key;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In that case, &lt;code&gt;pg_class.relreplident&lt;/code&gt; is set to &lt;code&gt;i&lt;/code&gt;, and &lt;code&gt;pg_index.indisreplident&lt;/code&gt; is set to true for that index. You can see which index is used with &lt;code&gt;\d&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# \d t
                Table "public.t"
 Column | Type | Collation | Nullable | Default 
--------+------+-----------+----------+---------
 a      | text |           | not null | 
Indexes:
    "t_a_key" UNIQUE CONSTRAINT, btree (a) REPLICA IDENTITY&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But what can we do to mess it up?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q1: What happens if you use a not-unique index?&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# create index idx_t_a on t (a);
CREATE INDEX
sender=# alter table t replica identity using index idx_t_a;
ERROR:  cannot use non-unique index "idx_t_a" as replica identity&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We can’t even set the &lt;code&gt;REPLICA IDENTITY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q2: What happens if you drop the index?&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text not null);
CREATE TABLE
sender=# create unique index idx_t_a on t (a);
CREATE INDEX
sender=# alter table t replica identity using index idx_t_a;
ALTER TABLE
sender=# drop index idx_t_a;
DROP INDEX
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Well you were able to drop it! The table’s &lt;code&gt;relreplident&lt;/code&gt; is still &lt;code&gt;i&lt;/code&gt;, but now there is no index with a true &lt;code&gt;pg_index.indisreplident&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let’s see what happens if we try to use it. On the subscriber:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;receiver=# create table t (a text);
CREATE TABLE
receiver=# create subscription s connection 'dbname=sender' publication p;
NOTICE:  created replication slot "s" on publisher
CREATE SUBSCRIPTION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Back on the sender:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# insert into t values ('a');
INSERT 0 1
sender=# update t set a = 'b';
ERROR:  cannot update table "t" because it does not have a replica identity and publishes updates&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Oops! We were able to insert, but the update failed. Even on the sender, we weren’t able to make the change.&lt;/p&gt;

&lt;p&gt;I’m dreaming of some other trickery with &lt;code&gt;ALTER INDEX&lt;/code&gt;, but nothing that seems problematic is supported, like changing a unique index to a non-unique one.&lt;/p&gt;

&lt;p&gt;So like &lt;code&gt;NOTHING&lt;/code&gt;, failures happen when we update the table. But in addition there is validation before setting &lt;code&gt;pg_index.indisreplident&lt;/code&gt;. If the index is not appropriate, we’ll fail there.&lt;/p&gt;

&lt;h3 id="_4"&gt;&lt;code&gt;DEFAULT&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;DEFAULT&lt;/code&gt; setting means to use the table’s primary key, if it has one. So if the table has a primary key, you’re all good. But what if it doesn’t? Is it the same as &lt;code&gt;NOTHING&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Well we are allowed to set things up:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And on the receiver:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;receiver=# create table t (a text);
CREATE TABLE
receiver=# create subscription s connection 'dbname=sender' publication p;
NOTICE:  created replication slot "s" on publisher
CREATE SUBSCRIPTION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we make some changes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# insert into t values ('a');
INSERT 0 1
sender=# update t set a = 'b';
ERROR:  cannot update table "t" because it does not have a replica identity and publishes updates&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So again we can insert things, but not send updates. We fail before we even send over the change. In this test we didn’t even need to run anything on the receiver to cause the failure.&lt;/p&gt;

&lt;p&gt;Since the failure is so late, there is no difference between starting with a primary key and dropping it partway through. Likewise with other ideas we tried for &lt;code&gt;NOTHING&lt;/code&gt;, like adding the publication before the table or using &lt;code&gt;FOR ALL TABLES&lt;/code&gt;. Also changing an insert-only publication to an update publication is permitted, but then the next update command fails.&lt;/p&gt;

&lt;p&gt;Still let’s just try one simple case:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q2: What happens if you drop the primary key from a table in a publication?&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text primary key);
CREATE TABLE
sender=# create publication p for table t;
CREATE PUBLICATION
sender=# alter table t drop constraint t_pkey;
ALTER TABLE
sender=# insert into t values ('a');
INSERT 0 1
sender=# update t set a = 'b';
ERROR:  cannot update table "t" because it does not have a replica identity and publishes updates
HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;No problem dropping the key, but then we get a failure making an update. Also this test shows that no subscription is required.&lt;/p&gt;

&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;So in all four cases, the validation happens when you update/delete a row and try to push the change through a &lt;code&gt;PUBLICATION&lt;/code&gt;. Your change gets rolled back, so there is no inconsistency. I’m happy about this, because it lowers the risk of tricking Postgres into publishing changes it shouldn’t. Also it makes things easier for replicating temporal tables. (Of course that was my ulterior motive!)&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-10-13:/posts/2024/10/logical-replication/</id>
    <title type="html">Postgres Logical Replication</title>
    <published>2024-10-13T00:00:00Z</published>
    <updated>2024-10-13T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/10/logical-replication/" type="text/html"/>
    <content type="html">
&lt;p&gt;Logical &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding.html"&gt;decoding&lt;/a&gt; is different from logical &lt;a href="https://www.postgresql.org/docs/current/logical-replication.html"&gt;replication&lt;/a&gt;. Logical replication is built on logical decoding.&lt;/p&gt;

&lt;h2 id="logical_decoding"&gt;Logical Decoding&lt;/h2&gt;

&lt;p&gt;Logical decoding exports the changes happening in your database. They are streamed through a &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS"&gt;replication slot&lt;/a&gt;. The slot keeps track of how far you’ve read, so that it doesn’t skip or repeat messages. (Btw how is it not “at least once” delivery?) Basically you are getting a stream of WAL records. But whereas a &lt;em&gt;physical&lt;/em&gt; replication slot gives you the exact binary of the WAL as it gets written, a logical replication slot includes an &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-output-plugin.html"&gt;&lt;em&gt;output plugin&lt;/em&gt;&lt;/a&gt; that encodes the WAL in a (hopefully) more accessible way.&lt;/p&gt;

&lt;p&gt;You can write output plugins yourself by implementing various callbacks to handle different kinds of database activity. The only essential callbacks are &lt;code&gt;LogicalDecodeBeginCB&lt;/code&gt;, &lt;code&gt;LogicalDecodeChangeCB&lt;/code&gt;, and &lt;code&gt;LogicalDecodeCommitCB&lt;/code&gt;, but there are many optional ones. Some of those let you implement two-phase commit (see e.g. &lt;a href="https://www.amazon.com/Designing-Data-Intensive-Applications-Reliable-Maintainable/dp/1449373321"&gt;Kleppmann&lt;/a&gt; 352–359).&lt;/p&gt;

&lt;p&gt;You can also add an &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-writer.html"&gt;&lt;em&gt;output writer&lt;/em&gt;&lt;/a&gt;, but I’m not sure how it differs from a plugin. Somehow it only requires implementing three callbacks instead of many. I don’t see any way to “install” your writer or attach it to a slot. I’ll come back to this some other day.&lt;/p&gt;

&lt;p&gt;The most popular output plugin is &lt;a href="https://github.com/eulerto/wal2json"&gt;wal2json&lt;/a&gt;. When you read from the replication slot, you get each WAL record as a JSON object. Postgres also has a built-in &lt;code&gt;test_decoding&lt;/code&gt; output plugin (the default), which gives you the details as plain text.&lt;/p&gt;

&lt;p&gt;Postgres comes with a tool called &lt;code&gt;pg_recvlogical&lt;/code&gt; you can use to read from a replication slot. It can also create and drop slots. For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;pg_recvlogical --create-slot --if-not-exists --slot s --dbname paul
pg_recvlogical --start --slot s --dbname paul -f -&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you let that run then go into psql and issue some inserts/updates/deletes, you will see the messages getting sent through the slot.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;paul=# create table t (a text primary key);
CREATE TABLE
paul=# insert into t values ('a'), ('b');
INSERT 0 2
paul=# update t set a = 'aa' where a = 'a';
UPDATE 1
paul=# delete from t where a = 'b';
DELETE 1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Our &lt;code&gt;pg_recvlogical&lt;/code&gt; command prints:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;BEGIN 19715
COMMIT 19715
BEGIN 19716
table public.t: INSERT: a[text]:'a'
table public.t: INSERT: a[text]:'b'
COMMIT 19716
BEGIN 19717
table public.t: UPDATE: old-key: a[text]:'a' new-tuple: a[text]:'aa'
COMMIT 19717
BEGIN 19718
table public.t: DELETE: a[text]:'b'
COMMIT 19718&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you want JSON instead, you can create the slot with &lt;code&gt;-P wal2json&lt;/code&gt; (after installing it).&lt;/p&gt;

&lt;p&gt;The tricky part here is identifying which row changed for updates and deletes. Because &lt;code&gt;t&lt;/code&gt; had a primary key, Postgres used it, because it should uniquely identify a row on the other end. But you can do other things too, based on the table’s &lt;code&gt;REPLICA IDENTITY&lt;/code&gt;. You can set this to &lt;code&gt;DEFAULT&lt;/code&gt; (i.e. using the primary key if present), &lt;code&gt;NOTHING&lt;/code&gt;, &lt;code&gt;USING INDEX &amp;lt;index&amp;gt;&lt;/code&gt;, or &lt;code&gt;FULL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NOTHING&lt;/code&gt; is the same as &lt;code&gt;DEFAULT&lt;/code&gt; with no primary key: there is no identifying information available.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;USING INDEX&lt;/code&gt; takes a unique index with no nullable parts, and uses that.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FULL&lt;/code&gt; uses all the attributes of the row. It works for any table, but it’s less performant.&lt;/p&gt;

&lt;p&gt;For more details on &lt;code&gt;REPLICA IDENTITY&lt;/code&gt; you can read my article &lt;a href="/posts/2024/10/postgres-replica-identity/"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s drop the table, create it without a primary key, and run the same commands again:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;paul=# create table t (a text);
CREATE TABLE
paul=# insert into t values ('a'), ('b');
INSERT 0 2
paul=# update t set a = 'aa' where a = 'a';
UPDATE 1
paul=# delete from t where a = 'b';
DELETE 1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the logical decoding output doesn’t have any old-row identifiers:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;BEGIN 19720
COMMIT 19720
BEGIN 19721
table public.t: INSERT: a[text]:'a'
table public.t: INSERT: a[text]:'b'
COMMIT 19721
BEGIN 19722
table public.t: UPDATE: a[text]:'aa'
COMMIT 19722
BEGIN 19723
table public.t: DELETE: (no-tuple-data)
COMMIT 19723&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Missing a &lt;code&gt;REPLICA IDENTITY&lt;/code&gt; is no problem for logical decoding. For logical &lt;em&gt;replication&lt;/em&gt;, it will raise an error when you try to update or delete.&lt;/p&gt;

&lt;h2 id="clarification_of_streaming"&gt;Clarification of “Streaming”&lt;/h2&gt;

&lt;p&gt;In the Postgres docs, &lt;a href="https://www.postgresql.org/message-id/flat/CA%2BrenyULt3VBS1cRFKUfT2%3D5dr61xBOZdAZ-CqX3XLGXqY-aTQ%40mail.gmail.com"&gt;the word “streaming” can be misleading&lt;/a&gt;. Often it means &lt;em&gt;physical&lt;/em&gt; replication (originally the only kind of streaming replication we had), in contrast to logical—but not always.&lt;/p&gt;

&lt;p&gt;Sometimes “streaming replication” constrasts with &lt;a href="https://www.postgresql.org/docs/current/warm-standby.html"&gt;“log shipping”&lt;/a&gt;. It means the standby opens a connection to the primary and constantly pulls data via the &lt;a href="https://www.postgresql.org/docs/current/protocol-replication.html"&gt;streaming replication protocol&lt;/a&gt;. In log shipping, the primary has an &lt;code&gt;archive_command&lt;/code&gt; to copy WAL files where the standby can read them. (In fact you usually combine these methods.) That said, this page is at the same time equating streaming replication with physical. You &lt;em&gt;could&lt;/em&gt; set up a standby with logical replication, but that’s not what it’s describing.&lt;/p&gt;

&lt;p&gt;In another place, “streaming” means the streaming replication protocol in contrast to &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-sql.html"&gt;SQL commands&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Perhaps most often, “streaming replication” means “physical replication” in contrast to logical. For instance &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS"&gt;this note about logical replication slots&lt;/a&gt; should perhaps &lt;code&gt;s/streaming/physical/&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL also has streaming replication slots (see Section 26.2.5), but they are used somewhat differently there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Likewise the three opening paragraphs of the &lt;a href="https://www.postgresql.org/docs/current/runtime-config-replication.html"&gt;page for “Replication” configuration&lt;/a&gt; seem to constrast streaming replication to logical replication.&lt;/p&gt;

&lt;p&gt;So just watch out when you’re reading! More precise language would contast “physical” with “logical”, and be clear that both are “streaming.” I’m hopeful from that mailing list thread so far that we might start moving the docs in that direction.&lt;/p&gt;

&lt;h2 id="logical_replication"&gt;Logical Replication&lt;/h2&gt;

&lt;p&gt;Logical replication is built on top of logical decoding and replication slots. But instead of working at such a low level, it replicates changes from one Postgres table to another (usually in another cluster). The publisher side uses a built-in output plugin named &lt;code&gt;pgoutput&lt;/code&gt;. To set it up, you use &lt;a href="https://www.postgresql.org/docs/current/sql-createpublication.html"&gt;&lt;code&gt;CREATE PUBLICATION&lt;/code&gt;&lt;/a&gt; on the sender and &lt;a href="https://www.postgresql.org/docs/current/sql-createsubscription.html"&gt;&lt;code&gt;CREATE SUBSCRIPTION&lt;/code&gt;&lt;/a&gt; on the receiver. Like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# CREATE PUBLICATION p FOR TABLE t1, t2;
. . .
receiver=# CREATE SUBSCRIPTION s CONNECTION 'host=yonder dbname=thisnthat' PUBLICATION p;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But first you’ll need to have tables in the receiving database named &lt;code&gt;t1&lt;/code&gt; and &lt;code&gt;t2&lt;/code&gt;, with at least enough columns to match their sources.&lt;/p&gt;

&lt;p&gt;There are lots of options. For the publication, you can create it &lt;code&gt;FOR ALL TABLES&lt;/code&gt; or &lt;code&gt;FOR TABLES IN SCHEMA s1, s2&lt;/code&gt;. You can publish a subset of event types (&lt;code&gt;insert&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;truncate&lt;/code&gt;). You can publish a subset of each table’s columns. You can do special things with partitions. You can have a &lt;code&gt;WHERE&lt;/code&gt; clause to publish only certain rows (assuming the publication is for a single table).&lt;/p&gt;

&lt;p&gt;The subscription side has lots of options too. See the docs above for details.&lt;/p&gt;

&lt;p&gt;Once you have a publication and subscription created, you will see an entry in the sender’s &lt;code&gt;pg_stat_replication&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# select * from pg_stat_replication \gx
-[ RECORD 1 ]----+------------------------------
pid              | 44344
usesysid         | 10
usename          | paul
application_name | s
client_addr      | NULL
client_hostname  | NULL
client_port      | -1
backend_start    | 2024-10-13 11:37:18.361684-05
backend_xmin     | NULL
state            | streaming
sent_lsn         | 0/11DE0AB8
write_lsn        | 0/11DE0AB8
flush_lsn        | 0/11DE0AB8
replay_lsn       | 0/11DE0AB8
write_lag        | NULL
flush_lag        | NULL
replay_lag       | NULL
sync_priority    | 0
sync_state       | async
reply_time       | 2024-10-13 11:37:48.419264-05&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Unlike physical replication, logical replication does not transfer DDL changes.&lt;/p&gt;

&lt;h3 id="synchronous_replication"&gt;Synchronous Replication&lt;/h3&gt;

&lt;p&gt;Both logical decoding and logical replication can be &lt;em&gt;synchronous&lt;/em&gt;. This means we can configure the primary to report successful commits only after hearing from the standby(s) that they were successful. (You can also do this with physical replication.) To enable this, first set &lt;code&gt;synchronous_standby_names&lt;/code&gt; to a comma-separated list of names you will wait for. Then set &lt;code&gt;synchronous_commit&lt;/code&gt; to &lt;code&gt;remote_apply&lt;/code&gt;, &lt;code&gt;on&lt;/code&gt;, &lt;code&gt;remote_write&lt;/code&gt;, or &lt;code&gt;local&lt;/code&gt;. (The default is &lt;code&gt;on&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;remote_apply&lt;/code&gt; is the most thorough: the standbys must flush the commit record to disk and apply it (so that other connections can see it). &lt;code&gt;on&lt;/code&gt; also requires flushing to disk (e.g. WAL), but it needn’t be applied. &lt;code&gt;remote_write&lt;/code&gt; requires standbys to perform the write to disk, but they need not have confirmation from the OS that it’s been flushed. And &lt;code&gt;local&lt;/code&gt; will not wait for standbys. (This is sort of pointless if you put things in &lt;code&gt;synchronous_standby_names&lt;/code&gt;, but maybe it lets you disable synchronous replication temporarily without erasing that other setting.)&lt;/p&gt;

&lt;p&gt;The names to put in &lt;code&gt;synchronous_standby_names&lt;/code&gt; should match the connection’s &lt;code&gt;application_name&lt;/code&gt;, one of the parameters available when opening any connection. You can set it in the connection string if you like. Otherwise it defaults to the subscription name. (For physical replication, standbys omitting this from their connection string default to their &lt;code&gt;cluster_name&lt;/code&gt; or failing that &lt;code&gt;walsender&lt;/code&gt;.) You can see the &lt;code&gt;application_name&lt;/code&gt; in &lt;code&gt;pg_stat_replication&lt;/code&gt; above matches the subscription name.&lt;/p&gt;

&lt;p&gt;If you have more than one standby listed as synchronous, the primary will wait on confirmation from &lt;em&gt;all&lt;/em&gt; of them (by default: you can do more nuanced things with &lt;code&gt;synchronous_standby_names&lt;/code&gt; if you like).&lt;/p&gt;

&lt;p&gt;If you are using synchronous logical replication, this is an &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-synchronous.html#LOGICALDECODING-SYNCHRONOUS-OVERVIEW"&gt;important warning&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A synchronous replica receiving changes via logical decoding will work in the scope of a single database. Since, in contrast to that, &lt;code&gt;synchronous_standby_names&lt;/code&gt; currently is server wide, this means this technique will not work properly if more than one database is actively used.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;According to &lt;a href="https://github.com/postgres/postgres/commit/3cb828dbe26087e7754f49f3cfe3ed036d5af439"&gt;commit 3cb828dbe2&lt;/a&gt; the problem is a potential deadlock from locking catalog tables. The commit and the docs have the specific lock sequences that are dangerous, and they don’t seem hard to avoid.&lt;/p&gt;

&lt;p&gt;My first impression on reading the note was a different scenario though. Consider: you have two databases, both replicated with logical replication, but each to a different standby. You make a change in one database, and now you’re waiting for both standbys to confirm. But one never got the change so will never answer.&lt;/p&gt;

&lt;p&gt;But in fact Postgres is smarter than that and only waits for the standbys that are relevant. I tested these scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One database in the sender cluster replicates to a database in the receiver cluster, and I update a table in a different database in the sender cluster. (I am “actively using” it.)&lt;/li&gt;

&lt;li&gt;Two databases in the sender cluster each replicate to the same database in the receiver cluster.&lt;/li&gt;

&lt;li&gt;Two databases in the sender cluster each replicate to databases in different clusters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id="logical_replication_combined_with_physical_replication_failover"&gt;Logical Replication combined with Physical Replication Failover&lt;/h3&gt;

&lt;p&gt;If your primary cluster has both a physical replication standby and logical replication subscribers, and then you fail over to the standby, you need a way to point the logical subscribers at the newly-promoted cluster (i.e. the former standby). You want to do that without losing data.&lt;/p&gt;

&lt;p&gt;If (before the failure) you set &lt;a href="https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-SYNC-REPLICATION-SLOTS"&gt;&lt;code&gt;sync_replication_slots&lt;/code&gt;&lt;/a&gt; to true on the physical standby, it will maintain the same slots the primary has, including keeping track of how far each has been read. That way when your logical subscribers connect to the new primary, they can resume where they left off.&lt;/p&gt;

&lt;p&gt;It is also a good idea to use &lt;a href="https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-SYNCHRONIZED-STANDBY-SLOTS"&gt;&lt;code&gt;synchronized_standby_slots&lt;/code&gt;&lt;/a&gt;, to make sure the logical subscribers don’t get &lt;em&gt;ahead&lt;/em&gt; of the physical standby.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-09-15:/posts/2024/09/benchmarking-temporal-foreign-keys/</id>
    <title type="html">Benchmarking Temporal Foreign Keys</title>
    <published>2024-09-15T00:00:00Z</published>
    <updated>2024-09-15T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/09/benchmarking-temporal-foreign-keys/" type="text/html"/>
    <content type="html">
&lt;p&gt;Way back in Februrary Peter Eisentraut &lt;a href="https://www.postgresql.org/message-id/7bd1c8f9-a91a-41a3-990e-0f796ba692ec%40eisentraut.org"&gt;asked me&lt;/a&gt; if I’d tested the performance of &lt;a href="https://commitfest.postgresql.org/49/4308/"&gt;my patch to add temporal foreign keys to Postgres&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Have you checked that the generated queries can use indexes and have suitable performance? Do you have example execution plans maybe?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is a report on the tests I made. I gave &lt;a href="/posts/2024/08/benchbase-and-temporal-foreign-keys-pdxpug-talk/"&gt;a talk about this&lt;/a&gt; last month at &lt;a href="https://pdxpug.wordpress.com/"&gt;pdxpug&lt;/a&gt;, but this blog post will be easier to access, and I’ll focus just on the foreign key results.&lt;/p&gt;

&lt;h2 id="method"&gt;Method&lt;/h2&gt;

&lt;p&gt;As far as I know there are no published benchmark schemas or workflows for temporal data. Since the tables require start/end columns, you can’t use an existing benchmark like &lt;a href="https://www.tpc.org/tpch/"&gt;TCP-H&lt;/a&gt;. The tables built in to &lt;a href="https://www.postgresql.org/docs/current/pgbench.html"&gt;pgbench&lt;/a&gt; are no use either. I’m not even sure where to find a public dataset. The closest is something called “Incumben”, mentioned in the &lt;a href="https://www.zora.uzh.ch/id/eprint/62963/1/p433-dignos.pdf"&gt;“Temporal Alignment” paper&lt;/a&gt;. They authors say it has 85,857 entries for job assignments across 49,195 employees at the University of Arizona—but I can’t find any trace of it online. (I’ll update here if I hear back from them about it.)&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/pjungwir/benchbase/tree/temporal"&gt;a temporal benchmark of my own&lt;/a&gt; using &lt;a href="https://github.com/cmu-db/benchbase"&gt;CMU’s Benchbase framework&lt;/a&gt;. (Thanks to &lt;a href="https://markwkm.blogspot.com/"&gt;Mark Wong&lt;/a&gt; and &lt;a href="https://github.com/grantholly"&gt;Grant Holly&lt;/a&gt; for that recommendation!) It also uses employees and positions, both temporal tables with a &lt;code&gt;valid_at&lt;/code&gt; column (a &lt;code&gt;daterange&lt;/code&gt;). Each position has a reference to an employee, checked by a temporal foreign key. Primary and foreign keys have GiST indexes combining the integer part and the range part. Here is the DDL:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-sql"&gt;&lt;span class="class"&gt;CREATE&lt;/span&gt; &lt;span class="type"&gt;TABLE&lt;/span&gt; employees (
    id          &lt;span class="predefined-type"&gt;int&lt;/span&gt; GENERATED &lt;span class="keyword"&gt;BY&lt;/span&gt; &lt;span class="directive"&gt;DEFAULT&lt;/span&gt; &lt;span class="keyword"&gt;AS&lt;/span&gt; IDENTITY &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    valid_at    daterange &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    name        &lt;span class="predefined-type"&gt;text&lt;/span&gt; &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    salary      &lt;span class="predefined-type"&gt;int&lt;/span&gt; &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    &lt;span class="directive"&gt;PRIMARY&lt;/span&gt; &lt;span class="type"&gt;KEY&lt;/span&gt; (id, valid_at WITHOUT OVERLAPS)
);

&lt;span class="class"&gt;CREATE&lt;/span&gt; &lt;span class="type"&gt;TABLE&lt;/span&gt; positions (
    id          &lt;span class="predefined-type"&gt;int&lt;/span&gt; GENERATED &lt;span class="keyword"&gt;BY&lt;/span&gt; &lt;span class="directive"&gt;DEFAULT&lt;/span&gt; &lt;span class="keyword"&gt;AS&lt;/span&gt; IDENTITY &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    valid_at    daterange &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    name        &lt;span class="predefined-type"&gt;text&lt;/span&gt; &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    employee_id &lt;span class="predefined-type"&gt;int&lt;/span&gt; &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;,
    &lt;span class="directive"&gt;PRIMARY&lt;/span&gt; &lt;span class="type"&gt;KEY&lt;/span&gt; (id, valid_at WITHOUT OVERLAPS),
    &lt;span class="directive"&gt;FOREIGN&lt;/span&gt; &lt;span class="type"&gt;KEY&lt;/span&gt; (employee_id, PERIOD valid_at) &lt;span class="keyword"&gt;REFERENCES&lt;/span&gt; employees (id, PERIOD valid_at)
);
&lt;span class="class"&gt;CREATE&lt;/span&gt; &lt;span class="type"&gt;INDEX&lt;/span&gt; idx_positions_employee_id &lt;span class="keyword"&gt;ON&lt;/span&gt; positions &lt;span class="keyword"&gt;USING&lt;/span&gt; gist (employee_id, valid_at);&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Naturally you can’t run that unless you’ve compiled Postgres with the temporal patches above.&lt;/p&gt;

&lt;p&gt;The benchmark has procedures that exercise foreign keys (update/delete employee, insert/update position). There are other procedures too: selecting one row, selecting many rows, inner join, outer join, semijoin, antijoin. I plan to add aggregates and set operations (union/except/intersect), as well as better queries for sequenced vs non-sequenced semantics. But right now the foreign key procedures are better developed than anything else. I also plan to change the SQL from rangetypes to standard SQL:2011 PERIODs, at least for non-Postgres RDBMSes. I’ll write more about all that later; this post is about foreign keys.&lt;/p&gt;

&lt;h3 id="_implementation"&gt;
&lt;code&gt;range_agg&lt;/code&gt; Implementation&lt;/h3&gt;

&lt;p&gt;Temporal foreign keys in Postgres are implemented like this:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-sql"&gt;&lt;span class="class"&gt;SELECT&lt;/span&gt; &lt;span class="integer"&gt;1&lt;/span&gt;
&lt;span class="keyword"&gt;FROM&lt;/span&gt;    (
  &lt;span class="class"&gt;SELECT&lt;/span&gt; pkperiodatt &lt;span class="keyword"&gt;AS&lt;/span&gt; r
  &lt;span class="keyword"&gt;FROM&lt;/span&gt;   [ONLY] pktable x
  &lt;span class="keyword"&gt;WHERE&lt;/span&gt;  pkatt1 = &lt;span class="error"&gt;$&lt;/span&gt;&lt;span class="integer"&gt;1&lt;/span&gt; [&lt;span class="keyword"&gt;AND&lt;/span&gt; ...]
  &lt;span class="keyword"&gt;AND&lt;/span&gt;    pkperiodatt &amp;amp;&amp;amp; &lt;span class="error"&gt;$&lt;/span&gt;n
&lt;span class="keyword"&gt;FOR&lt;/span&gt; &lt;span class="type"&gt;KEY&lt;/span&gt; SHARE &lt;span class="keyword"&gt;OF&lt;/span&gt; x
) x1
&lt;span class="keyword"&gt;HAVING&lt;/span&gt; &lt;span class="error"&gt;$&lt;/span&gt;n &amp;lt;&lt;span class="error"&gt;@&lt;/span&gt; range_agg(x1.r)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is very similar to non-temporal checks. The main difference is we use &lt;code&gt;range_agg&lt;/code&gt; to aggregate referenced records, since it may require their combination to satisfy the reference. For example if the employee got a raise in the middle of the position, neither employee record alone covers the position’s valid time:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/employee_position_fk.png" alt="Temporal foreign key"&gt;&lt;/p&gt;

&lt;p&gt;In our query, the &lt;code&gt;HAVING&lt;/code&gt; checks that the “sum” of the employee times covers the position time.&lt;/p&gt;

&lt;p&gt;A subquery is not logically required, but Postgres currently doesn’t allow &lt;code&gt;FOR KEY SHARE&lt;/code&gt; in a query with aggregations.&lt;/p&gt;

&lt;p&gt;I like this query because it works not just for rangetypes, but multiranges too. In fact we could easily support arbitrary types, as long as the user provides an opclass with an appropriate support function (similar to the &lt;code&gt;stratnum&lt;/code&gt; support function introduced for temporal primary keys). We would call that function in place of &lt;code&gt;range_agg&lt;/code&gt;. But how does it perform?&lt;/p&gt;

&lt;h3 id=""&gt;&lt;code&gt;EXISTS implementation&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;I compared this query with two others. The original implementation for temporal foreign keys appears on pages 128–129 of &lt;a href="https://www2.cs.arizona.edu/~rts/tdbbook.pdf"&gt;&lt;em&gt;Developing Time-Oriented Database Applications in SQL&lt;/em&gt; by Richard Snodgrass&lt;/a&gt;. I call this the “&lt;code&gt;EXISTS&lt;/code&gt; implementation”. Here is the SQL I used:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-sql"&gt;&lt;span class="class"&gt;SELECT&lt;/span&gt; &lt;span class="integer"&gt;1&lt;/span&gt;
&lt;span class="comment"&gt;-- There was a PK when the FK started:&lt;/span&gt;
&lt;span class="keyword"&gt;WHERE&lt;/span&gt; &lt;span class="keyword"&gt;EXISTS&lt;/span&gt;
  &lt;span class="class"&gt;SELECT&lt;/span&gt;  &lt;span class="integer"&gt;1&lt;/span&gt;
  &lt;span class="keyword"&gt;FROM&lt;/span&gt;    [ONLY] &amp;lt;pktable&amp;gt;
  &lt;span class="keyword"&gt;WHERE&lt;/span&gt;   pkatt1 = &lt;span class="error"&gt;$&lt;/span&gt;&lt;span class="integer"&gt;1&lt;/span&gt; [&lt;span class="keyword"&gt;AND&lt;/span&gt; ...]
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(lower(pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
       &amp;lt;= COALESCE(lower(&lt;span class="error"&gt;$&lt;/span&gt;n), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(lower(&lt;span class="error"&gt;$&lt;/span&gt;n), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
       &amp;lt;  COALESCE(upper(pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
)
&lt;span class="comment"&gt;-- There was a PK when the FK ended:&lt;/span&gt;
&lt;span class="keyword"&gt;AND&lt;/span&gt; &lt;span class="keyword"&gt;EXISTS&lt;/span&gt; (
  &lt;span class="class"&gt;SELECT&lt;/span&gt;  &lt;span class="integer"&gt;1&lt;/span&gt;
  &lt;span class="keyword"&gt;FROM&lt;/span&gt;    [ONLY] &amp;lt;pktable&amp;gt;
  &lt;span class="keyword"&gt;WHERE&lt;/span&gt;   pkatt1 = &lt;span class="error"&gt;$&lt;/span&gt;&lt;span class="integer"&gt;1&lt;/span&gt; [&lt;span class="keyword"&gt;AND&lt;/span&gt; ...]
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(lower(pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
       &amp;lt;  COALESCE(upper(&lt;span class="error"&gt;$&lt;/span&gt;n), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(upper(&lt;span class="error"&gt;$&lt;/span&gt;n), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
       &amp;lt;= COALESCE(upper(pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
)
&lt;span class="comment"&gt;-- There are no gaps in the PK:&lt;/span&gt;
&lt;span class="comment"&gt;-- (i.e. there is no PK that ends early,&lt;/span&gt;
&lt;span class="comment"&gt;-- unless a matching PK record starts right away)&lt;/span&gt;
&lt;span class="keyword"&gt;AND&lt;/span&gt; &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="keyword"&gt;EXISTS&lt;/span&gt; (
  &lt;span class="class"&gt;SELECT&lt;/span&gt;  &lt;span class="integer"&gt;1&lt;/span&gt;
  &lt;span class="keyword"&gt;FROM&lt;/span&gt;    [ONLY] &amp;lt;pktable&amp;gt; &lt;span class="keyword"&gt;AS&lt;/span&gt; pk1
  &lt;span class="keyword"&gt;WHERE&lt;/span&gt;   pkatt1 = &lt;span class="error"&gt;$&lt;/span&gt;&lt;span class="integer"&gt;1&lt;/span&gt; [&lt;span class="keyword"&gt;AND&lt;/span&gt; ...]
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(lower(&lt;span class="error"&gt;$&lt;/span&gt;n), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
       &amp;lt;  COALESCE(upper(pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(upper(pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
       &amp;lt;  COALESCE(upper(&lt;span class="error"&gt;$&lt;/span&gt;n), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
  &lt;span class="keyword"&gt;AND&lt;/span&gt;     &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="keyword"&gt;EXISTS&lt;/span&gt; (
    &lt;span class="class"&gt;SELECT&lt;/span&gt;  &lt;span class="integer"&gt;1&lt;/span&gt;
    &lt;span class="keyword"&gt;FROM&lt;/span&gt;    [ONLY] &amp;lt;pktable&amp;gt; &lt;span class="keyword"&gt;AS&lt;/span&gt; pk2
    &lt;span class="keyword"&gt;WHERE&lt;/span&gt;   pk1.pkatt1 = pk2.pkatt1 [&lt;span class="keyword"&gt;AND&lt;/span&gt; ...]
            &lt;span class="comment"&gt;-- but skip pk1.pkperiodatt &amp;amp;&amp;amp; pk2.pkperiodatt&lt;/span&gt;
    &lt;span class="keyword"&gt;AND&lt;/span&gt;     COALESCE(lower(pk2.pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
         &amp;lt;= COALESCE(upper(pk1.pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
            COALESCE(upper(pk1.pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
         &amp;lt;  COALESCE(upper(pk2.pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
  )
);&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The main idea here is that we check three things: (1) the referencing row is covered in the beginning, (2) it is covered in the end, (3) in between, the referenced row(s) have no gaps.&lt;/p&gt;

&lt;p&gt;I made a few changes to the original:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It can’t be a &lt;code&gt;CHECK&lt;/code&gt; constraint, since it references other rows.&lt;/li&gt;

&lt;li&gt;There is less nesting. The original is wrapped in a big &lt;code&gt;NOT EXISTS&lt;/code&gt; and looks for bad rows. Essentially it says “there are no invalid records.” In Postgres we check one referencing row at a time, and we give a result if it is valid. You could say we look for good rows. This also requires inverting the middle-layer &lt;code&gt;EXISTS&lt;/code&gt; and &lt;code&gt;NOT EXISTS&lt;/code&gt; predicates, and changing &lt;code&gt;OR&lt;/code&gt;s to &lt;code&gt;AND&lt;/code&gt;s. I’ve &lt;a href="https://www.cybertec-postgresql.com/en/avoid-or-for-better-performance/"&gt;often run into trouble with &lt;code&gt;OR&lt;/code&gt;&lt;/a&gt;, so this is probably fortunate.&lt;/li&gt;

&lt;li&gt;We have to “unwrap” the start/end times since they are stored in a rangetype. I could have used rangetype operators here, but I wanted to keep the adaptation as straightforward as possible, and the previous changes felt like a lot already. Unwrapping requires dealing with unbounded ranges, so I’m using plus/minus &lt;code&gt;Infinity&lt;/code&gt; as a sentinel. This is not perfectly accurate, since in ranges a null bound is “further out” than a plus/minus &lt;code&gt;Infinity&lt;/code&gt;. (Try &lt;code&gt;select '{(,)}'::datemultirange - '{(-Infinity,Infinity)}'::datemultirange&lt;/code&gt;.) But again, solving that was taking me too far from the original, and it’s fine for a benchmark.&lt;/li&gt;

&lt;li&gt;We need to lock the rows with &lt;code&gt;FOR KEY SHARE&lt;/code&gt; in the same way as above. We need to do this in each branch, since they may use different rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given the complexity, I didn’t expect this query to perform very well.&lt;/p&gt;

&lt;h3 id="_implementation_2"&gt;
&lt;code&gt;lag&lt;/code&gt; implementation&lt;/h3&gt;

&lt;p&gt;Finally there is an implementation in &lt;a href="https://github.com/xocolatl/periods"&gt;Vik Fearing’s &lt;code&gt;periods&lt;/code&gt; extension&lt;/a&gt;. This is a lot like the &lt;code&gt;EXISTS&lt;/code&gt; implementation, except to check for gaps it uses the &lt;code&gt;lag&lt;/code&gt; window function. Here is the SQL I tested:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-sql"&gt;&lt;span class="class"&gt;SELECT&lt;/span&gt;  &lt;span class="integer"&gt;1&lt;/span&gt;
&lt;span class="keyword"&gt;FROM&lt;/span&gt;    (
  &lt;span class="class"&gt;SELECT&lt;/span&gt;  uk.uk_start_value,
          uk.uk_end_value,
          NULLIF(LAG(uk.uk_end_value) &lt;span class="keyword"&gt;OVER&lt;/span&gt;
            (&lt;span class="keyword"&gt;ORDER&lt;/span&gt; &lt;span class="keyword"&gt;BY&lt;/span&gt; uk.uk_start_value), uk.uk_start_value) &lt;span class="keyword"&gt;AS&lt;/span&gt; x
  &lt;span class="keyword"&gt;FROM&lt;/span&gt;   (
    &lt;span class="class"&gt;SELECT&lt;/span&gt;  coalesce(lower(x.pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;-Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;) &lt;span class="keyword"&gt;AS&lt;/span&gt; uk_start_value,
            coalesce(upper(x.pkperiodatt), &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;Infinity&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;) &lt;span class="keyword"&gt;AS&lt;/span&gt; uk_end_value
    &lt;span class="keyword"&gt;FROM&lt;/span&gt;    pktable &lt;span class="keyword"&gt;AS&lt;/span&gt; x
    &lt;span class="keyword"&gt;WHERE&lt;/span&gt;   pkatt1 = &lt;span class="error"&gt;$&lt;/span&gt;&lt;span class="integer"&gt;1&lt;/span&gt; [&lt;span class="keyword"&gt;AND&lt;/span&gt; ...]
    &lt;span class="keyword"&gt;AND&lt;/span&gt;     uk.pkperiodatt &amp;amp;&amp;amp; &lt;span class="error"&gt;$&lt;/span&gt;n
    &lt;span class="keyword"&gt;FOR&lt;/span&gt; &lt;span class="type"&gt;KEY&lt;/span&gt; SHARE &lt;span class="keyword"&gt;OF&lt;/span&gt; x
  ) &lt;span class="keyword"&gt;AS&lt;/span&gt; uk
) &lt;span class="keyword"&gt;AS&lt;/span&gt; uk
&lt;span class="keyword"&gt;WHERE&lt;/span&gt;   uk.uk_start_value &amp;lt; upper(&lt;span class="error"&gt;$&lt;/span&gt;n)
&lt;span class="keyword"&gt;AND&lt;/span&gt;     uk.uk_end_value &amp;gt;= lower(&lt;span class="error"&gt;$&lt;/span&gt;n)
&lt;span class="keyword"&gt;HAVING&lt;/span&gt;  &lt;span class="predefined"&gt;MIN&lt;/span&gt;(uk.uk_start_value) &amp;lt;= lower(&lt;span class="error"&gt;$&lt;/span&gt;n)
&lt;span class="keyword"&gt;AND&lt;/span&gt;     &lt;span class="predefined"&gt;MAX&lt;/span&gt;(uk.uk_end_value) &amp;gt;= upper(&lt;span class="error"&gt;$&lt;/span&gt;n)
&lt;span class="keyword"&gt;AND&lt;/span&gt;     array_agg(uk.x) FILTER (&lt;span class="keyword"&gt;WHERE&lt;/span&gt; uk.x &lt;span class="keyword"&gt;IS&lt;/span&gt; &lt;span class="keyword"&gt;NOT&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;) &lt;span class="keyword"&gt;IS&lt;/span&gt; &lt;span class="predefined-constant"&gt;NULL&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Again I had to make some adaptations to &lt;a href="https://github.com/xocolatl/periods/blob/328c1aaac731f44958b725fb02ca75186f501ce7/periods--1.2.sql#L2230-L2252"&gt;the original&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is less nesting, for similar reasons as before.&lt;/li&gt;

&lt;li&gt;We unwrap the ranges, much like the &lt;code&gt;EXISTS&lt;/code&gt; version. Again there is an &lt;code&gt;Infinity&lt;/code&gt;-vs-null discrepancy, but it is harder to deal with since the query uses null entries in the &lt;code&gt;lag&lt;/code&gt; result to indicate gaps.&lt;/li&gt;

&lt;li&gt;I couldn’t resist using &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; instead of &lt;code&gt;&amp;lt;=&lt;/code&gt; and &lt;code&gt;&amp;gt;=&lt;/code&gt; in the most-nested part to find relevant rows. The change was sufficiently obvious, and if it makes a difference it should speed things up, so it makes the comparison a bit more fair.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I made a new branch rooted in my &lt;a href="https://github.com/pjungwir/postgresql/tree/valid-time"&gt;valid-time branch&lt;/a&gt;, and added &lt;a href="https://github.com/pjungwir/postgresql/tree/temporal-fk-comparison"&gt;an extra commit&lt;/a&gt; to switch between each implementation with a compile-tag flag. By default we still use &lt;code&gt;range_agg&lt;/code&gt;, but instead you can say &lt;code&gt;‑DRI_TEMPORAL_IMPL_LAG&lt;/code&gt; or &lt;code&gt;‑DRI_TEMPORAL_IMPL_EXISTS&lt;/code&gt;. I installed each implementation in a separate cluster, listening on port 5460, 5461, and 5462 respectively.&lt;/p&gt;

&lt;p&gt;I also included procedures in Benchbase to simply run the above queries as &lt;code&gt;SELECT&lt;/code&gt;s. Since we are doing quite focused microbenchmarking here, I thought that would be less noisy than doing the same DML for each implementation. It also means we can run a mix of all three implementations together: they use the same cluster, and if there is any noise on the machine it affects them all. If you look at my temporal benchmark code, you’ll see the same SQL but adapted for the &lt;code&gt;employees&lt;/code&gt;/&lt;code&gt;positions&lt;/code&gt; tables.&lt;/p&gt;

&lt;h2 id="hypothesis"&gt;Hypothesis&lt;/h2&gt;

&lt;p&gt;Here is the query plan for the &lt;code&gt;range_agg&lt;/code&gt; implementation:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Aggregate
  Filter: ('[2020-10-10,2020-12-12)'::daterange &amp;lt;@ range_agg(x1.r))
  -&amp;gt;  Subquery Scan on x1
    -&amp;gt;  LockRows
      -&amp;gt;  Index Scan using employees_pkey on employees x
        Index Cond: ((id = 500) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It uses the index, and it all seems like what we’d want. It is not an &lt;code&gt;Index Only Scan&lt;/code&gt;, but that’s because we lock the rows. Non-temporal foreign keys are the same way. This should perform pretty well.&lt;/p&gt;

&lt;p&gt;Here is the query plan for the &lt;code&gt;EXISTS&lt;/code&gt; implementation:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Result
  One-Time Filter: ((InitPlan 1).col1 AND (InitPlan 2).col1 AND (NOT (InitPlan 4).col1))
  InitPlan 1
  -&amp;gt;  LockRows
    -&amp;gt;  Index Scan using employees_pkey on employees x
      Index Cond: ((id = 500) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))
      Filter: ((COALESCE(lower(valid_at), '-infinity'::date) &amp;lt;= '2020-10-10'::date) AND ('2020-10-10'::date &amp;lt; COALESCE(upper(valid_at), 'infinity'::date)))
  InitPlan 2
  -&amp;gt;  LockRows
    -&amp;gt;  Index Scan using employees_pkey on employees x_1
      Index Cond: ((id = 500) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))
      Filter: ((COALESCE(lower(valid_at), '-infinity'::date) &amp;lt; '2020-12-12'::date) AND ('2020-12-12'::date &amp;lt;= COALESCE(upper(valid_at), 'infinity'::date)))
  InitPlan 4
  -&amp;gt;  LockRows
    -&amp;gt;  Index Scan using employees_pkey on employees pk1
      Index Cond: ((id = 500) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))
      Filter: (('2020-10-10'::date &amp;lt; COALESCE(upper(valid_at), 'infinity'::date)) AND (COALESCE(upper(valid_at), 'infinity'::date) &amp;lt; '2020-12-12'::date) AND (NOT EXISTS(SubPlan 3)))
      SubPlan 3
      -&amp;gt;  LockRows
        -&amp;gt;  Index Scan using employees_pkey on employees pk2
          Index Cond: (id = pk1.id)
          Filter: ((COALESCE(lower(valid_at), '-infinity'::date) &amp;lt;= COALESCE(upper(pk1.valid_at), 'infinity'::date)) AND (COALESCE(upper(pk1.valid_at), 'infinity'::date) &amp;lt; COALESCE(upper(valid_at), 'infinity'::date)))&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That looks like a lot of work!&lt;/p&gt;

&lt;p&gt;And here is the plan for the &lt;code&gt;lag&lt;/code&gt; implementation:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Aggregate
  Filter: ((array_agg(uk.x) FILTER (WHERE (uk.x IS NOT NULL)) IS NULL) AND (min(uk.uk_start_value) &amp;lt;= '2020-10-10'::date) AND (max(uk.uk_end_value) &amp;gt;= '2020-12-12'::date))
  -&amp;gt;  Subquery Scan on uk
    Filter: ((uk.uk_start_value &amp;lt; '2020-12-12'::date) AND (uk.uk_end_value &amp;gt;= '2020-10-10'::date))
    -&amp;gt;  WindowAgg
      -&amp;gt;  Sort
        Sort Key: uk_1.uk_start_value
        -&amp;gt;  Subquery Scan on uk_1
          -&amp;gt;  LockRows
            -&amp;gt;  Index Scan using employees_pkey on employees x
              Index Cond: ((id = 500) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This looks a lot like the &lt;code&gt;range_agg&lt;/code&gt; version. We still use our index. There is an extra &lt;code&gt;Sort&lt;/code&gt; step, but internally the &lt;code&gt;range_agg&lt;/code&gt; function must do much the same thing (if not something worse). Maybe the biggest difference (though a slight one) is aggregating twice.&lt;/p&gt;

&lt;p&gt;So I expect &lt;code&gt;range_agg&lt;/code&gt; to perform the best, with &lt;code&gt;lag&lt;/code&gt; a close second, and &lt;code&gt;EXISTS&lt;/code&gt; far behind.&lt;/p&gt;

&lt;p&gt;One exception may be a single referencing row that spans many referenced rows. If &lt;code&gt;range_agg&lt;/code&gt; is O(n&lt;sup&gt;2&lt;/sup&gt;), it should fall behind as the referenced rows increase.&lt;/p&gt;

&lt;h2 id="results"&gt;Results&lt;/h2&gt;

&lt;p&gt;I started by running a quick test on my laptop, an M2 Macbook Air with 16 GB of RAM. I tested the DML commands on each cluster, one after another. Then I checked the benchbase summary file for the throughput. The results were what I expected:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/throughput-comparison-2024-07-28.png" alt="early results"&gt;&lt;/p&gt;

&lt;p&gt;Similarly, &lt;code&gt;range_agg&lt;/code&gt; had the best latency at the 25th, 50th, 75th, 90th, and 99th percentiles:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/latency-comparison-2024-07-28.png" alt="latency comparison"&gt;&lt;/p&gt;

&lt;p&gt;But the difference throughout is pretty small, and at the time my Benchbase procedures used a lot of &lt;code&gt;synchronized&lt;/code&gt; blocks to ensure there were few foreign key failures, and that kind of locking seemed like it might throw off the results. I needed to do more than this casual check.&lt;/p&gt;

&lt;p&gt;I ran all the future benchmarks on my personal desktop, running Ubuntu 22.04.&lt;/p&gt;
&lt;!--
TODO: fill in these details
cpu
ram
nvme
linux version
postgres 18devel
--&gt;
&lt;p&gt;It was hard to make things reproducible, but I wrote various scripts as I went, and I tried to capture results. The repo for all that is &lt;a href="https://github.com/pjungwir/benchmarking-temporal-tables"&gt;here&lt;/a&gt;. My pdxpug talk above contains some reflections about improving my benchmark methodology.&lt;/p&gt;

&lt;p&gt;I also removed the &lt;code&gt;synchronized&lt;/code&gt; blocks and dealt with foreign key failures a better way (by categorizing them as errors but not raising an exception).&lt;/p&gt;

&lt;p&gt;The first more careful tests used the direct &lt;code&gt;SELECT&lt;/code&gt; statements.&lt;/p&gt;

&lt;p&gt;Again, the 95th percentile latency was what I expected:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/95th-latency-comparison-half-invalid.png" alt="95% latency comparison"&gt;&lt;/p&gt;

&lt;p&gt;But the winner for mean latency was &lt;code&gt;EXISTS&lt;/code&gt;!:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/mean-latency-comparison-half-invalid.png" alt="mean latency comparison"&gt;&lt;/p&gt;

&lt;p&gt;A clue was in the Benchbase output showing successful transactions vs errors. (The &lt;code&gt;Noop&lt;/code&gt; procedure is so can make the proportions 33/33/33/1 instead of 33/33/34.):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Completed Transactions:
com.oltpbenchmark.benchmarks.temporal.procedures.CheckForeignKeyRangeAgg/01      [72064] ********************************************************************************
com.oltpbenchmark.benchmarks.temporal.procedures.CheckForeignKeyLag/02           [71479] *******************************************************************************
com.oltpbenchmark.benchmarks.temporal.procedures.CheckForeignKeyExists/03        [71529] *******************************************************************************
com.oltpbenchmark.benchmarks.temporal.procedures.Noop/04                         [ 4585] *****
Aborted Transactions:
&amp;lt;EMPTY&amp;gt;

Rejected Transactions (Server Retry):
&amp;lt;EMPTY&amp;gt;

Rejected Transactions (Retry Different):
&amp;lt;EMPTY&amp;gt;

Unexpected SQL Errors:
com.oltpbenchmark.benchmarks.temporal.procedures.CheckForeignKeyRangeAgg/01      [80861] ********************************************************************************
com.oltpbenchmark.benchmarks.temporal.procedures.CheckForeignKeyLag/02           [80764] *******************************************************************************
com.oltpbenchmark.benchmarks.temporal.procedures.CheckForeignKeyExists/03        [80478] *******************************************************************************&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;More than half of the transactions were an invalid reference.&lt;/p&gt;

&lt;p&gt;And if we put one of those into &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;, we see that most of the plan was never executed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Result (actual time=0.034..0.035 rows=0 loops=1)
  One-Time Filter: ((InitPlan 1).col1 AND (InitPlan 2).col1 AND (NOT (InitPlan 4).col1))
  InitPlan 1
  -&amp;gt;  LockRows (actual time=0.033..0.033 rows=0 loops=1)
    -&amp;gt;  Index Scan using employees_pkey on employees x (actual time=0.033..0.033 rows=0 loops=1)
      Index Cond: ((id = 5999) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))
      Filter: ((COALESCE(lower(valid_at), '-infinity'::date) &amp;lt;= '2020-10-10'::date) AND ('2020-10-10'::date &amp;lt; COALESCE(upper(valid_at), 'infinity'::date)))
  InitPlan 2
  -&amp;gt;  LockRows (never executed)
    -&amp;gt;  Index Scan using employees_pkey on employees x_1 (never executed)
      Index Cond: ((id = 5999) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))
      Filter: ((COALESCE(lower(valid_at), '-infinity'::date) &amp;lt; '2020-12-12'::date) AND ('2020-12-12'::date &amp;lt;= COALESCE(upper(valid_at), 'infinity'::date)))
  InitPlan 4
  -&amp;gt;  LockRows (never executed)
    -&amp;gt;  Index Scan using employees_pkey on employees pk1 (never executed)
      Index Cond: ((id = 5999) AND (valid_at &amp;amp;&amp;amp; '[2020-10-10,2020-12-12)'::daterange))
      Filter: (('2020-10-10'::date &amp;lt; COALESCE(upper(valid_at), 'infinity'::date)) AND (COALESCE(upper(valid_at), 'infinity'::date) &amp;lt; '2020-12-12'::date) AND (NOT EXISTS(SubPlan 3)))
      SubPlan 3
      -&amp;gt;  LockRows (never executed)
        -&amp;gt;  Index Scan using employees_pkey on employees pk2 (never executed)
          Index Cond: (id = pk1.id)
          Filter: ((COALESCE(lower(valid_at), '-infinity'::date) &amp;lt;= COALESCE(upper(pk1.valid_at), 'infinity'::date)) AND (COALESCE(upper(pk1.valid_at), 'infinity'::date) &amp;lt; COALESCE(upper(valid_at), 'infinity'::date)))&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In this example, the beginning of the referencing range wasn’t covered, so Postgres never had to check the rest. Essentially the query is &lt;code&gt;a AND b AND c&lt;/code&gt;, so Postgres can short-circuit the evaluation as soon as it finds &lt;code&gt;a&lt;/code&gt; to be false. Using &lt;code&gt;range_agg&lt;/code&gt; or &lt;code&gt;lag&lt;/code&gt; doesn’t allow this, because an aggregate/window function has to run to completion to get a result.&lt;/p&gt;

&lt;p&gt;As confirmation (a bit gratuitous to be honest), I ran the &lt;code&gt;EXISTS&lt;/code&gt; benchmark with this bpftrace script:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Count how many exec nodes per query were required,
// and print a histogram of how often each count happens.
// Run this for each FK implementation separately.
// My hypothesis is that the EXISTS implementation calls ExecProcNode far fewer times,
// but only if the FK is invalid.

u:/home/paul/local/bench-*/bin/postgres:standard_ExecutorStart {
  @nodes[tid] = 0
}
u:/home/paul/local/bench-*/bin/postgres:ExecProcNode {
  @nodes[tid] += 1
}
u:/home/paul/local/bench-*/bin/postgres:standard_ExecutorEnd {
  @calls = hist(@nodes[tid]);
  delete(@nodes[tid]);
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For &lt;code&gt;EXISTS&lt;/code&gt; I got this histogram when there were no invalid references:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;@calls:
[0]                    6 |                                                    |
[1]                    0 |                                                    |
[2, 4)                 0 |                                                    |
[4, 8)            228851 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[8, 16)                1 |                                                    |
[16, 32)               1 |                                                    |
[32, 64)               2 |                                                    |
[64, 128)              2 |                                                    |
[128, 256)             2 |                                                    |
[256, 512)             5 |                                                    |&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But with 50%+ errors I got this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;@calls:
[0]                    6 |                                                    |
[1]                    0 |                                                    |
[2, 4)            218294 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[4, 8)            183438 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@         |
[8, 16)              231 |                                                    |
[16, 32)               1 |                                                    |
[32, 64)               2 |                                                    |
[64, 128)              2 |                                                    |
[128, 256)             2 |                                                    |
[256, 512)             5 |                                                    |&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So more than half the time, Postgres ran the query with half the steps (maybe one-fourth).&lt;/p&gt;

&lt;p&gt;After tuning the random numbers to bring errors closer to 1%, I got results more like the original ones. Mean latency:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/mean-latency-comparison-mostly-valid.png" alt="mostly valid mean latency comparison"&gt;&lt;/p&gt;

&lt;p&gt;Median latency:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/median-latency-comparison-mostly-valid.png" alt="mostly valid median latency comparison"&gt;&lt;/p&gt;

&lt;p&gt;95th percentile latency:&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/2024-09/95th-latency-comparison-mostly-valid.png" alt="mostly valid 95% latency comparison"&gt;&lt;/p&gt;

&lt;h2 id="conclusions"&gt;Conclusions&lt;/h2&gt;

&lt;p&gt;All foreign key implementations have expected query plans. We use indexes where we should, etc.&lt;/p&gt;

&lt;p&gt;When most foreign key references are valid, &lt;code&gt;range_agg&lt;/code&gt; outperforms the other two implementations by a small but consistent amount. But with a large number of invalid references, &lt;code&gt;EXISTS&lt;/code&gt; is a lot faster.&lt;/p&gt;

&lt;p&gt;In most applications I’ve seen, foreign keys are used as guardrails, and we expect 99% of checks to pass (or more really). When using &lt;code&gt;ON DELETE CASCADE&lt;/code&gt; the situation is different, but these benchmarks are for &lt;code&gt;NO ACTION&lt;/code&gt; or &lt;code&gt;RESTRICT&lt;/code&gt;, and I don’t think &lt;code&gt;CASCADE&lt;/code&gt; affords the &lt;code&gt;EXISTS&lt;/code&gt; implementation the same shortcuts. So it seems right to optimize for the mostly-valid case, not the more-than-half-invalid case.&lt;/p&gt;

&lt;p&gt;These results are good news, because &lt;code&gt;range_agg&lt;/code&gt; is also more general: it supports multiranges and custom types.&lt;/p&gt;

&lt;h2 id="further_work"&gt;Further Work&lt;/h2&gt;

&lt;p&gt;There are more things I’d like to benchmark (and if I do I’ll update this post):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace separate start/end comparisons with range operators in the &lt;code&gt;EXISTS&lt;/code&gt; and &lt;code&gt;lag&lt;/code&gt; implementations. I just need to make sure they still pass all the tests when I do that.&lt;/li&gt;

&lt;li&gt;Correct the &lt;code&gt;Infinity&lt;/code&gt;-vs-null discrepancy.&lt;/li&gt;

&lt;li&gt;Monitor the CPU and disk activity under each implementation and compare the results. I don’t think I’ll see any difference in disk, but CPU might be interesting.&lt;/li&gt;

&lt;li&gt;Compare different scale factors (i.e. starting number of employees/positions).&lt;/li&gt;

&lt;li&gt;Compare implementations when an employee is chopped into many small records, and a single position spans all of them. If &lt;code&gt;range_agg&lt;/code&gt; is O(n&lt;sup&gt;2&lt;/sup&gt;) that should be worse than the sorting in the other options.&lt;/li&gt;

&lt;li&gt;Compare temporal foreign keys to non-temporal foreign keys (based on B-tree indexes, not GiST). I’m not sure yet how to do this in a meaningful way. Of course b-trees are faster in general, but how do I use them to achieve the same primary key and foreign key constraints? Maybe the best way is to create the tables without constraints, give them only b-tree indexes, and run the direct &lt;code&gt;SELECT&lt;/code&gt; statements, not the DML.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-08-26:/posts/2024/08/benchbase-and-temporal-foreign-keys-pdxpug-talk/</id>
    <title type="html">PDXPUG Talk: Benchbase and Temporal Foreign Keys</title>
    <published>2024-08-26T00:00:00Z</published>
    <updated>2024-08-26T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/08/benchbase-and-temporal-foreign-keys-pdxpug-talk/" type="text/html"/>
    <content type="html">
&lt;p&gt;Last Thursday I gave &lt;a href="https://illuminatedcomputing.com/pages/pdxpug2024-benchbase-and-temporal-foreign-keys/"&gt;a talk at PDXPUG about using Benchbase to compare the performance of temporal foreign keys&lt;/a&gt;. It was a lot of fun, and a really good turnout. There were even folks from Seattle and Bend. After listening for an hour, people stuck around and talked about databases and benchmarks for another two, then the last few holdouts went out for drinks for another hour and a half. At least half the audience were way more qualified to give the talk than me. To my surprise &lt;a href="http://smalldatum.blogspot.com/"&gt;Mark Callaghan&lt;/a&gt; was there, who has published database benchmarks non-stop for years.&lt;/p&gt;

&lt;p&gt;I had two major goals: &lt;a href="/posts/2024/08/benchbase-documentation/"&gt;to document how to use Benchbase&lt;/a&gt; and to report on comparing three implementations of temporal foreign keys. A couple minor goals were to share the start of a broader general-purpose benchmark for temporal databases and to talk about a benchmarking methodology, especially mistakes I made and how I tried to improve.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-07-17:/posts/2024/07/temporal-ops/</id>
    <title type="html">Temporal Ops</title>
    <published>2024-07-17T00:00:00Z</published>
    <updated>2024-07-17T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/07/temporal-ops/" type="text/html"/>
    <content type="html">
&lt;p&gt;One silver lining of &lt;a href="/posts/2024/07/temporal-reverted/"&gt;temporal primary &amp;amp; foreign keys getting reverted&lt;/a&gt; is I got to meet &lt;a href="https://github.com/hettie-d"&gt;Hettie Dombrovskaya&lt;/a&gt; and &lt;a href="https://www.red-gate.com/simple-talk/author/borisnovikov/"&gt;Boris Novikov&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’ve been working with them to write SQL for various temporal operations not covered by the SQL:2011 standard. There is no support there for outer joins, semijoins, antijoins, aggregates, or set operations (&lt;code&gt;UNION&lt;/code&gt;, &lt;code&gt;INTERSECT&lt;/code&gt;, &lt;code&gt;EXCEPT&lt;/code&gt;). As far as I know no one has ever shown how to implement those operations in SQL. I have queries so far for outer join, semijoin, and antijoin, and I’m planning to include aggregates based on &lt;a href="https://www.red-gate.com/simple-talk/databases/postgresql/making-temporal-databases-work-part-2-computing-aggregates-across-temporal-versions/"&gt;this article by Boris&lt;/a&gt;. The set operations look pretty easy to me, so hopefully I’ll have those soon too.&lt;/p&gt;

&lt;p&gt;If you’re interested, the repo is &lt;a href="https://github.com/pjungwir/temporal_ops"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-07-05:/posts/2024/07/temporal-reverted/</id>
    <title type="html">Temporal Reverted</title>
    <published>2024-07-05T00:00:00Z</published>
    <updated>2024-07-05T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/07/temporal-reverted/" type="text/html"/>
    <content type="html">
&lt;p&gt;My work adding temporal primary keys and foreign keys to Postgres was &lt;a href="https://www.postgresql.org/message-id/47550967-260b-4180-9791-b224859fe63e@illuminatedcomputing.com"&gt;reverted from v17&lt;/a&gt;. The problem is empty ranges (and multiranges). An empty range doesn’t overlap anything, including another empty range. So &lt;code&gt;'empty' &amp;amp;&amp;amp; 'empty'&lt;/code&gt; is false. But temporal PKs are essentially an exclusion constraint using &lt;code&gt;(id WITH =, valid_at WITH &amp;amp;&amp;amp;)&lt;/code&gt;. Therefore you can insert duplicates, as long as the range is empty:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;INSERT INTO t (id, valid_at, name) VALUES (5, 'empty', 'foo');
INSERT INTO t (id, valid_at, name) VALUES (5, 'empty', 'bar');&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That might be okay for some users, but it surely breaks expectations for others. And it’s a questionable thing to do that we should probably just forbid. The SQL standard forbids empty &lt;code&gt;PERIOD&lt;/code&gt;s, so we should make sure that using plain ranges does the same. Adding a record with an empty application time doesn’t really have a meaning in the temporal model.&lt;/p&gt;

&lt;p&gt;I think this is a pretty small bump in the road. At &lt;a href="https://2024.pgconf.dev"&gt;the Postgres developers conference&lt;/a&gt; we found a good solution to excluding empty ranges. My original attempt used &lt;code&gt;CHECK&lt;/code&gt; constraints, but that had a lot of complications. Forbidding them in the executor is a lot simpler. I’ve already sent in &lt;a href="https://www.postgresql.org/message-id/56de0a38-77cc-48a8-bfa7-eb92fa57830b%40illuminatedcomputing.com"&gt;a new set of patches for v18&lt;/a&gt; that implement that change.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-01-24:/posts/2024/01/temporal-pks-merged/</id>
    <title type="html">Temporal PKs Merged!</title>
    <published>2024-01-24T00:00:00Z</published>
    <updated>2024-01-24T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/01/temporal-pks-merged/" type="text/html"/>
    <content type="html">
&lt;p&gt;&lt;strong&gt;UPDATE:&lt;/strong&gt; My temporal patches &lt;a href="https://illuminatedcomputing.com/posts/2024/07/temporal-reverted/"&gt;were reverted from v17&lt;/a&gt;. Hopefully they will be accepted for v18 instead.&lt;/p&gt;

&lt;p&gt;Today first thing in the morning I saw that &lt;a href="https://www.postgresql.org/message-id/88518c81-dcdc-4c5b-9200-146b74a520ab%40eisentraut.org"&gt;the first part of my temporal tables work for Postgres got merged&lt;/a&gt;. It was two patches actually: a little one to add a new GiST support function and then the main patch adding support for temporal primary keys and unique constraints based on range types. The support for SQL:2011 PERIODs comes later; for now you must use ranges—although in my opinion that is better anyway. Also this patch allows multiranges or, keeping with Postgres’s long history of extensibility, any type with an overlaps operator. So unless some big problem appears, PKs and UNIQUE constraints are on track to be released in Postgres 17.&lt;/p&gt;

&lt;p&gt;Probably I can get (basic) foreign keys into v17 too. Temporal update/delete, foreign keys with CASCADE, and PERIODs will more likely take ’til 18.&lt;/p&gt;

&lt;p&gt;If you are interested in temporal features, early testing is always appreciated! :-)&lt;/p&gt;

&lt;p&gt;Getting this into Postgres has been a ten-year journey, and the rest of this post is going to be a self-indulgent history of that work. You’ve been warned. :-)&lt;/p&gt;

&lt;p&gt;It started in 2013 when I kept noticing my clients needed a better way to track the history of things that change over time, and I discovered &lt;a href="https://www2.cs.arizona.edu/~rts/publications.html"&gt;Richard Snodgrass’s book &lt;em&gt;Developing Time-Oriented Database Applications in SQL&lt;/em&gt;&lt;/a&gt;. He offered a rigorous, systematic approach, with working SQL solutions for everything. This was exactly what I needed. His approach was vastly better than the ad hoc history-tracking I’d seen so far. But no one had implemented any of it!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/message-id/CA+renyVepHxTO1c7dFbVjP1GYMUc0-3qDNWPN30-noo5MPyaVQ@mail.gmail.com"&gt;My first Postgres patch&lt;/a&gt; in 2015 was motivated by temporal databases: I added UUID support to the &lt;code&gt;btree_gist&lt;/code&gt; extension. A temporal primary key is basically an exclusion constraint on &lt;code&gt;(id WITH =, valid_at WITH &amp;amp;&amp;amp;)&lt;/code&gt;, and I had a project with UUID ids. But that exclusion constraint requires a GiST index that knows how to perform equal comparisons against the &lt;code&gt;id&lt;/code&gt; column and overlap comparisons against the &lt;code&gt;valid_at&lt;/code&gt; column. Out-the-box GiST indexes can’t do that (unless your ids are something weird like range types). If your ids are integers, you can install &lt;code&gt;btree_gist&lt;/code&gt; to create a GiST opclass that knows what integer &lt;code&gt;=&lt;/code&gt; means, but at the time UUIDs were not supported. So I started there. I liked that temporal databases had a manageable feature set and a manageable body of literature, so that even a working programmer like me could break new ground (not like Machine Learning or even Time Series databases). Nonetheless that patch took a year and a half to get committed, and it was really other people like Chris Bandy who finished it.&lt;/p&gt;

&lt;p&gt;I kept reading about temporal databases, and in 2017 I wrote &lt;a href="https://github.com/pjungwir/time_for_keys/commit/b0146278cf0d25a6d3f05a38e2ab2c6e8001c93c"&gt;a proof-of-concept for temporal foreign keys&lt;/a&gt;, mostly at AWS Re:Invent. I happened to be given a free registration &amp;amp; hotel room, but it was too late to register for any of the good talks. But all that time with nothing to do was fantastically productive, and I remember by the flight home I was adding tons of tests, trying to cover every feature permutation—ha, as if. A few days after I returned I also published my &lt;a href="https://illuminatedcomputing.com/posts/2017/12/temporal-databases-bibliography/"&gt;annotated bibliography&lt;/a&gt;, which I’ve updated many times since.&lt;/p&gt;

&lt;p&gt;In Snodgrass a temporal foreign key is a page-and-a-half of SQL, mostly because a referencing row may need more than one referenced row to completely cover its time span. But I realized we could make the check much simpler if we used an aggregate function to combine all the relevant rows in the referenced table first. So I wrote &lt;code&gt;range_agg&lt;/code&gt;, first &lt;a href="https://github.com/pjungwir/range_agg"&gt;as an extension&lt;/a&gt;, then &lt;a href="https://www.postgresql.org/message-id/16d71dc8-34cf-5ebd-1ce5-ccd93c0a14f9@illuminatedcomputing.com"&gt;as a core patch&lt;/a&gt;. Jeff Davis (who laid the foundation for temporal support with range types and exclusion constraints) said my function was too narrow and pushed me to implement &lt;a href="https://commitfest.postgresql.org/31/2112/"&gt;multiranges&lt;/a&gt;, a huge improvement. Again it took a year and a half, and I had trouble making consistent progress. There was a lot of work at the end by Alvaro Herrera and Alexander Korotkov (and I’m sure others) to get it committed. That was a few days before Christmas 2020.&lt;/p&gt;

&lt;p&gt;Although the Postgres review process can take a long time, I cherish how it pushes me to do better. As a consultant/freelancer I encounter codebases of, hmm, varying quality, and Postgres gives me an example of what high standards look like.&lt;/p&gt;

&lt;p&gt;One thing I still remember from reading &lt;a href="https://www.amazon.com/Programmers-Work-Interviews-Computer-Industry/dp/1556152116"&gt;&lt;em&gt;Programmers at Work&lt;/em&gt;&lt;/a&gt; many years ago was how many inteviewees said they tried to build things at a higher level of abstraction than they thought they’d need. I’ve seen enough over-engineered tangles and inner-platform effects that my own bias is much closer to YAGNI and keeping things concrete, but the advice in those interviews still prods me to discover good abstractions. The Postgres codebase is full of things like that, and really it’s such a huge project that strong organizing ideas are essential. Multiranges was a great example of how to take a concrete need and convert it into something more general-purpose. And I thought I was doing that already with &lt;code&gt;range_agg&lt;/code&gt;! I think one thing that makes an abstraction good is a kind of definiteness, something opinionated. So it is not purely general, but really adds something new. It always requires an act of creation.&lt;/p&gt;

&lt;p&gt;The coolest thing I’ve heard of someone doing with multiranges was &lt;a href="https://iopscience.iop.org/article/10.3847/1538-3881/ac5ab8"&gt;using them in astronomy to search for neutrinos, gravitational waves, and gamma-ray bursts&lt;/a&gt;. By using multiranges, they were able to compare observations with maps of the night sky “orders of magnitude faster” than with other implementations. (Hopefully I’ve got that right: I read a pre-print of the paper but it was not all easy for me to understand!)&lt;/p&gt;

&lt;p&gt;My first patch for an actual temporal feature was &lt;a href="https://www.postgresql.org/message-id/CA%2BrenyWxfXpThaOXiNuo6dEJQPYOWjysnXQw7_m7WJnNHVn_-g%40mail.gmail.com"&gt;primary keys&lt;/a&gt; back in 2018. Then foreign keys followed in 2019, just a couple weeks before I gave a talk at PgCon about temporal databases. By the end of the year I had &lt;code&gt;FOR PORTION OF&lt;/code&gt; as well. At first &lt;code&gt;FOR PORTION OF&lt;/code&gt; was implemented in the Executor Phase, but when I gave a progress report for PgCon 2020 I was already working on a trigger-based reimplementation, though it wasn’t submitted until June 2021. I also pulled in &lt;a href="https://www.postgresql.org/message-id/mSRBIYry-zk13wGeWVYCGw5o0LqZ4dyectlawH43VoLzQ70Tqa2oClVdkmQ1MlhG2lToRBkyY77g1o7vSGUwMS9BXvE-H-bg_x9bjx0DKNI%3D%40protonmail.com"&gt;work by Vik Fearing from 2018&lt;/a&gt; to support &lt;code&gt;ADD/DROP PERIOD&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Soon after that progress got harder: my wife and I had our sixth baby in August, and somehow he seemed to be more work than the others. I took over daily math lessons (we homeschool), and I had to let go my biggest client, who needed more hours than I could give. (I’m proud to have given them an orderly transition over several months though.) In January 2022 Peter Eisentraut gave me a thorough review, but I went silent. Still, I had a lot of encouragement from the community, especially Corey Huinker, and eventually doing Postgres got easier again. I had a talk accepted for PgCon 2023, and I worked hard to submit new patches, which I did only weeks before the conference.&lt;/p&gt;

&lt;p&gt;The best part of PgCon was getting everyone who cared about my work together in the hallway to agree on the overall approach. I had worried for years about using ranges as well as PERIODs, since the standard doesn’t know anything about ranges. The second-best part was when someone told me I should stop calling myself a Postgres newbie.&lt;/p&gt;

&lt;p&gt;At PgCon Peter asked me to re-organize the patches, essentially implementing PERIODs as &lt;code&gt;GENERATED&lt;/code&gt; range columns. It made the code much nicer. I also went back to an Executor Phase approach for &lt;code&gt;FOR PORTION OF&lt;/code&gt;. Using triggers had some problems around updateable views and &lt;code&gt;READ COMMITTED&lt;/code&gt; transaction isolation.&lt;/p&gt;

&lt;p&gt;Since May I’ve felt more consistent than during my other Postgres work. I’ve been kept busy by excellent feedback by a meticulous reviewer, Jian He, who has caught many bugs. Often as soon as I get caught up, before I’ve written the email with the new patch files, he finds more things!&lt;/p&gt;

&lt;p&gt;Another thing that’s helped is going out once a week (for nearly a year now) to get early dinner then work on Postgres at a local bar. Somehow it’s much easier to do Postgres from somewhere besides my home office, where I do all my normal work. Getting dinner lets me read something related (lately &lt;a href="https://www.amazon.com/Designing-Data-Intensive-Applications-Reliable-Maintainable/dp/1449373321"&gt;&lt;em&gt;Designing Data-Intensive Applications&lt;/em&gt; by Martin Klepmann&lt;/a&gt; and &lt;a href="https://postgrespro.com/community/books/internals"&gt;&lt;em&gt;PostgreSQL 14 Internals&lt;/em&gt; by Egor Rogov&lt;/a&gt;), and it’s fun. Doing just a little every week helps me keep momentum, so that fitting in further progress here and there seems easy. I’m lucky to have a wife who has supported it so often, despite leaving her with the kids and dishes.&lt;/p&gt;

&lt;p&gt;I think I have years more work of temporal features to add, first finishing SQL:2011 then going beyond (e.g. temporal outer joins, temporal aggregates, temporal upsert). It’s been a great pleasure!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2023-11-06:/posts/2023/11/git-for-postgres-hacking/</id>
    <title type="html">Git for Postgres Hacking</title>
    <published>2023-11-06T00:00:00Z</published>
    <updated>2023-11-06T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2023/11/git-for-postgres-hacking/" type="text/html"/>
    <content type="html">
&lt;p&gt;In Postgres development it’s normal for patch attempts to require many revisions and last a long time. I just sent in &lt;a href="https://www.postgresql.org/message-id/2d9740ad-3bb7-473c-8441-344351caa8ee%40illuminatedcomputing.com"&gt;v17&lt;/a&gt; of my &lt;a href="https://commitfest.postgresql.org/44/4308/"&gt;SQL:2011 application time patch&lt;/a&gt;. The commitfest entry dates back to summer of 2021, but it’s really a continuation of &lt;a href="https://www.postgresql.org/message-id/flat/20200930073908.GQ1996%40paquier.xyz#c63d0a97f3c7bee005a1840164f00688"&gt;this thread from 2018&lt;/a&gt;. And it’s not yet done.&lt;/p&gt;

&lt;p&gt;My &lt;a href="https://commitfest.postgresql.org/31/2112/"&gt;work on multiranges&lt;/a&gt; is a similar story: 1.5 years from first patch to committed.&lt;/p&gt;

&lt;p&gt;Today I saw &lt;a href="https://jvns.ca/blog/2023/11/06/rebasing-what-can-go-wrong-/"&gt;this post&lt;/a&gt; by Julia Evans about problems people have with git rebase (&lt;a href="https://news.ycombinator.com/item?id=38164046"&gt;also see the hn discussion&lt;/a&gt;), and it reminded me of my struggles handling long-lived branches.&lt;/p&gt;

&lt;p&gt;In my early days with git I avoided rebasing, because I wanted the history to be authentic. Nowaday I rebase pretty freely, both to move my commits on top of the latest &lt;code&gt;master&lt;/code&gt; branch work and to interactively clean things up so the commits show logical progress (with generous commit messages explaining the motivation and broad design decisions: the “why”).&lt;/p&gt;

&lt;p&gt;But in my paid client work, PRs get merged pretty fast. There is nothing like the multi-year wait of Postgres hacking. Often I’ve wished for more history there. It’s not my day job, so it’s hard to remember fine details about something from months or years ago. And I’ve changed direction a couple times, and sometimes I want a way to consult that old history.&lt;/p&gt;

&lt;p&gt;But with Postgres you don’t have any choice but to rebase. You send your patch files to a mailing list, and if they don’t apply cleanly no one will look at them. I’ve spent hours and hours rebasing patches because the underlying systems changed before they could get committed.&lt;/p&gt;

&lt;p&gt;With multiranges this was tough, but at least it was just one patch file. Application time is a series of five patches, which over time have changed order and evolved from four. When it’s time to send a new version, I run &lt;code&gt;git format-patch&lt;/code&gt;, which turns each commit into a &lt;code&gt;.patch&lt;/code&gt; file. So I need to wind up with five well-groomed commits rebased on the latest &lt;code&gt;master&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My personal copy of the postgres repo on github has &lt;a href="https://github.com/pjungwir/postgresql/branches/all"&gt;a bunch of silly-named branches&lt;/a&gt; for stashing work when I want to change direction, so the history isn’t totally lost. But for a long time I had no system. It feels like when you see a spreadsheet named &lt;code&gt;Annual Report - Copy of Jan 7.bak - final - FINAL.xls&lt;/code&gt;. After all these years it’s unmanageable. (Okay at least I know not to name any Postgres submission “final”! ;-)&lt;/p&gt;

&lt;p&gt;I think I finally found a way to keep history that works for me. On my main &lt;a href="https://github.com/pjungwir/postgresql/commits/valid-time"&gt;&lt;code&gt;valid-time&lt;/code&gt; branch&lt;/a&gt; I keep a series of commits for each small change. I rebase to move them up and down, so that they will squash cleanly into the five commits I need at the end. You can see that I have one main commit for each of the five patches, but each is followed by many commits named &lt;code&gt;fixup pks: fixed this&lt;/code&gt; or &lt;code&gt;fixup fks: feedback from so-and-so&lt;/code&gt;. I rebase on &lt;code&gt;master&lt;/code&gt; every so often. I force-push all the time, since no one else uses the repo. (I do work on both a laptop and a desktop though, so I have to remember to &lt;code&gt;git fetch &amp;amp;&amp;amp; git reset --hard origin/valid-time&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;When I’m ready to submit new patches, I take a snapshot with &lt;code&gt;git checkout -b valid-time-v17-pre-squash&lt;/code&gt; and “make a backup” with &lt;code&gt;git push -u&lt;/code&gt;. Then I make a branch to squash things (&lt;code&gt;git checkout -b valid-time-v17&lt;/code&gt;). I do a &lt;code&gt;git rebase -i HEAD~60&lt;/code&gt;, press &lt;code&gt;*&lt;/code&gt; on &lt;code&gt;pick&lt;/code&gt;, type &lt;code&gt;cw fixup&lt;/code&gt;, then &lt;code&gt;n.n.n.n.n.n.&lt;/code&gt;, etc. ’til I have just the five commits. Then I have a script to do a clean build + test on each commit, since I want things to work at every point. While that’s running I write the email about the new patch, and hopefully send it in.&lt;/p&gt;

&lt;p&gt;So now I’m capturing the fine-grained history that went into each submission, and that won’t change no matter how aggressively I rebase the current work. I’m pretty happy with this flow. I wish I had started years ago.&lt;/p&gt;

&lt;p&gt;One git feature I could &lt;em&gt;almost&lt;/em&gt; use is &lt;code&gt;git rebase -i --autosquash&lt;/code&gt;. (Here are some &lt;a href="https://thoughtbot.com/blog/autosquashing-git-commits"&gt;articles&lt;/a&gt; &lt;a href="https://andrewlock.net/smoother-rebases-with-auto-squashing-git-commits/"&gt;about&lt;/a&gt; &lt;a href="https://git-scm.com/docs/git-rebase"&gt;it&lt;/a&gt;.) If your commit messages are named &lt;code&gt;fixup! foo&lt;/code&gt;, then git will automatically set those commits to &lt;code&gt;fixup&lt;/code&gt;, not &lt;code&gt;pick&lt;/code&gt;, and it will move them to just below whatever commit matches &lt;code&gt;foo&lt;/code&gt;. I follow this pattern but with &lt;code&gt;fixup&lt;/code&gt; not &lt;code&gt;fixup!&lt;/code&gt;, to keep it all manual. At first I just didn’t trust it (or myself).&lt;/p&gt;

&lt;p&gt;Now I’m ready to move to this workflow, but I’m not sure how to “match” one of my five main commits. I want a meaningful title (i.e. the first line of the commit message) for each little commit, so I use short abbreviations for the patch they target, e.g. &lt;code&gt;fixup pks: Add documentation for pg_constraint.contemporal column&lt;/code&gt;. Git doesn’t know that it should match &lt;code&gt;pks&lt;/code&gt; to &lt;code&gt;Add temporal PRIMARY KEY and UNIQUE constraints&lt;/code&gt; and ignore everything after the colon. If there were a way to preserve tags after a rebase I think I could tag the main commit as &lt;code&gt;pks&lt;/code&gt; and it &lt;em&gt;might&lt;/em&gt; work (but maybe not with the extra stuff after the colon).&lt;/p&gt;

&lt;p&gt;You can have git generate the new commit message for you with &lt;code&gt;git commit --fixup $sha&lt;/code&gt;, but it just copies the whole title verbatim, which is not what I want. Also who wants to remember &lt;code&gt;$sha&lt;/code&gt; for those five parent commits? And finally, I want to move these commits into place immediately, so I can build &amp;amp; test against each patch as I work. Git can’t move them for me without squashing them.&lt;/p&gt;

&lt;p&gt;The Thoughtbot article linked above says you can use a regex, e.g. &lt;code&gt;git commit --fixup :/pks&lt;/code&gt;, but: (1) The regex is used immediately to find the parent, but it gets replaced with that parent’s title. It doesn’t stay in your commit message. (2) If you give an additional commit message, it goes two lines below the &lt;code&gt;fixup!&lt;/code&gt; line, so it’s not in the commit title. This only solves having to remember &lt;code&gt;$sha&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What I really want is &lt;code&gt;fixup! ^: blah blah blah&lt;/code&gt; where &lt;code&gt;^&lt;/code&gt; means “the closest non-squashed parent”, &lt;strong&gt;and&lt;/strong&gt; the &lt;code&gt;^&lt;/code&gt; is resolved at rebase time, not commit time, &lt;strong&gt;and&lt;/strong&gt; everything after the colon is not used for matching. (If it needs to be a regex then &lt;code&gt;:/.&lt;/code&gt; is sufficient too.)&lt;/p&gt;

&lt;p&gt;Anyway I’m using my manual process for now, since with vim I can change 60 &lt;code&gt;pick&lt;/code&gt;s to &lt;code&gt;fixup&lt;/code&gt; in a few seconds. I’m not willing to lose meaningful titles to save a few seconds with &lt;code&gt;fixup!&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nonetheless it would be nice to have one less step I have to remember. Involuntarily I keep thinking about how I can make this feature work for me. If someone has a suggestion, please do let me know.&lt;/p&gt;

&lt;p&gt;Another approach is “stacked commits”. I went as far as installing &lt;a href="https://github.com/arxanas/git-branchless"&gt;git branchless&lt;/a&gt; and reading the docs &lt;a href="https://jg.gg/2018/09/29/stacked-diffs-versus-pull-requests/"&gt;and&lt;/a&gt; &lt;a href="https://kastiglione.github.io/git/2020/09/11/git-stacked-commits.html"&gt;some&lt;/a&gt; &lt;a href="https://andrewlock.net/working-with-stacked-branches-in-git-is-easier-with-update-refs/"&gt;articles&lt;/a&gt;, but to be honest I never went beyond a few tests, and I haven’t thought about it for a few months. It’s in the back of my head to give it a more honest effort.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2023-09-29:/posts/2023/09/custom-postgres-ubuntu-style/</id>
    <title type="html">Custom Postgres Ubuntu Style</title>
    <published>2023-09-29T00:00:00Z</published>
    <updated>2023-09-29T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2023/09/custom-postgres-ubuntu-style/" type="text/html"/>
    <content type="html">
&lt;p&gt;Ubuntu has a very nice way of organizing multiple versions of Postgres. They all get their own directories, and the commands dispatch to the latest version or something else if you set the &lt;code&gt;PGCLUSTER&lt;/code&gt; envvar or give a &lt;code&gt;--cluster&lt;/code&gt; option. For instance if you have installed Postgres 14, you will see files in &lt;code&gt;/usr/lib/postgresql/14&lt;/code&gt; and &lt;code&gt;/usr/share/postgresql/14&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Postgres a single installation is called a “cluster”. It has nothing to do with using multiple machines; it’s just the traditional term for the collection of configuration, data files, a postmaster process listening on a given port and its helper processes, etc.&lt;/p&gt;

&lt;p&gt;Elsewhere in the postgres world you say &lt;code&gt;initdb&lt;/code&gt; to create a cluster. In Ubuntu you say &lt;code&gt;pg_createcluster&lt;/code&gt;. By default Ubuntu creates a cluster named &lt;code&gt;main&lt;/code&gt; for each version you install. This gives you directories like &lt;code&gt;/etc/postgresql/14/main&lt;/code&gt; (for configuration) and &lt;code&gt;/var/lib/postgresql/14/main&lt;/code&gt; (for the data). The log file is &lt;code&gt;/var/log/postgresql/postgresql-14-main.log&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to run an old version of &lt;code&gt;pg_dump&lt;/code&gt;, you can say &lt;code&gt;PGCLUSTER=10/main pg_dump --version&lt;/code&gt; or &lt;code&gt;pg_dump --cluster=10/main --version&lt;/code&gt;. Likewise for &lt;code&gt;pg_restore&lt;/code&gt;, etc. (but—sidequest spolier alert—not &lt;code&gt;psql&lt;/code&gt; or a couple other things: see the footnote for more).&lt;/p&gt;

&lt;p&gt;One command that sadly &lt;em&gt;doesn’t&lt;/em&gt; support this is &lt;code&gt;pg_config&lt;/code&gt;, which is used to build custom extensions. Personally I just patch my local copy (or actually add a patched version earlier in the path, in my &lt;code&gt;~/bin&lt;/code&gt;), like this:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-bash"&gt;#!/bin/sh

# If postgresql-server-dev-* is installed, call pg_config from the latest
# available one. Otherwise fall back to libpq-dev's version.
#
# (C) 2011 Martin Pitt &amp;lt;mpitt@debian.org&amp;gt;
# (C) 2014-2016 Christoph Berg &amp;lt;myon@debian.org&amp;gt;
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.

set -e
PGBINROOT="/usr/lib/postgresql/"
#redhat# PGBINROOT="/usr/pgsql-"

# MY CHANGES START HERE
if [ -n "$PGCLUSTER" ]; then
  exec "$PGBINROOT/$PGCLUSTER/bin/pg_config" "$@"
fi
# MY CHANGES END HERE

LATEST_SERVER_DEV=`ls -v $PGBINROOT*/bin/pg_config 2&amp;gt;/dev/null|tail -n1`

if [ -n "$LATEST_SERVER_DEV" ]; then
    exec "$LATEST_SERVER_DEV" "$@"
else
    if [ -x /usr/bin/pg_config.libpq-dev ]; then
        exec /usr/bin/pg_config.libpq-dev "$@"
    else
        echo "You need to install postgresql-server-dev-X.Y for building a server-side extension or libpq-dev for building a client-side application." &amp;gt;&amp;amp;2
        exit 1
    fi
fi&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Without those changes you can’t build custom C extensions against old versions of Postgres. I’ve mentioned this in the past in &lt;a href="https://stackoverflow.com/a/43403193"&gt;this Stackoverflow answer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But that’s not what this post is about!&lt;/p&gt;

&lt;p&gt;This post is about compiling your own Postgres that you can manage like other Postgres versions on Ubuntu. I want an install that includes my temporal patches, so I can convert my timetracking app to use real temporal features. I want the files to live in the normal places, and I want it to start/stop the normal way.&lt;/p&gt;

&lt;p&gt;I’ve been hacking on Postgres for many years (Last May someone at PGCon told me I should stop calling myself a newbie. . . .), and I’ve always used &lt;code&gt;./configure --prefix=~/local ...&lt;/code&gt; to keep a dev installation. But I’ve never used it for anything durable. It’s just handy for &lt;code&gt;make installcheck&lt;/code&gt; and psql’ing and attaching a debugger. I blow it away all the time with &lt;code&gt;rm -rf ~/local/pgsql/data &amp;amp;&amp;amp; ~/local/bin/initdb -D ~/local/pgsql/data&lt;/code&gt;. I crash it all the time because that’s how it goes when I’m writing C. ;-) That’s not where my timetracking data should live.&lt;/p&gt;

&lt;p&gt;My first attempt was to build Postgres like this:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-bash"&gt;version=17devel
./configure \
  'CFLAGS=-ggdb -Og -g3 -fno-omit-frame-pointer' \
  --enable-tap-tests --enable-cassert --enable-debug \
  --prefix=/usr/lib/postgresql/${version} \
  --datarootdir=/usr/share/postgresql/${version}
make clean &amp;amp;&amp;amp; make world &amp;amp;&amp;amp; sudo make install-world&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(I might as well keep some dev stuff in there in case I need it.)&lt;/p&gt;

&lt;p&gt;Then as the &lt;code&gt;postgres&lt;/code&gt; user I tried this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;postgres@tal:~$ pg_createcluster 17devel main
Error: invalid version '17devel'&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Alas!&lt;/p&gt;

&lt;p&gt;Ubuntu’s multi-version system is controlled by the &lt;code&gt;postgresql-common&lt;/code&gt; package, so I got the source for it by running &lt;code&gt;apt-get source postgresql-common&lt;/code&gt;. (You might need to uncomment a &lt;code&gt;deb-src&lt;/code&gt; line in &lt;code&gt;/etc/apt/sources.list&lt;/code&gt; and run &lt;code&gt;sudo apt-get update&lt;/code&gt;.) Grepping for “invalid version” I found the message in &lt;code&gt;pg_createcluster&lt;/code&gt; from these lines:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-perl"&gt;my ($version) = $ARGV[0] =~ /^(\d+\.?\d+)$/;
error "invalid version '$ARGV[0]'" unless defined $version;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Instead of fighting with the system I decided to call it version 30. It worked!&lt;/p&gt;

&lt;p&gt;Except I had one last problem:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;postgres@tal:~$ psql -p 5443
psql: error: connection to server on socket "/tmp/.s.PGSQL.5443" failed: No such file or directory
        Is the server running locally and accepting connections on that socket?&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The issue is that the postgresql-common infrastructure dispatches to the latest tools by default, and our “version 30” psql is looking in the wrong place for a socket file. In &lt;code&gt;postgresql.conf&lt;/code&gt; you can see this line:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And taking a peek we have:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;paul@tal:~$ ls -A /var/run/postgresql/
10-main.pg_stat_tmp  13-main.pid           9.4-main.pg_stat_tmp  .s.PGSQL.5433.lock  .s.PGSQL.5437       .s.PGSQL.5440.lock
10-main.pid          14-main.pg_stat_tmp   9.4-main.pid          .s.PGSQL.5434       .s.PGSQL.5437.lock  .s.PGSQL.5441
11-main.pg_stat_tmp  14-main.pid           9.5-main.pg_stat_tmp  .s.PGSQL.5434.lock  .s.PGSQL.5438       .s.PGSQL.5441.lock
11-main.pid          15-main.pid           9.5-main.pid          .s.PGSQL.5435       .s.PGSQL.5438.lock  .s.PGSQL.5442
12-main.pg_stat_tmp  30-main.pid           9.6-main.pg_stat_tmp  .s.PGSQL.5435.lock  .s.PGSQL.5439       .s.PGSQL.5442.lock
12-main.pid          9.3-main.pg_stat_tmp  9.6-main.pid          .s.PGSQL.5436       .s.PGSQL.5439.lock  .s.PGSQL.5443
13-main.pg_stat_tmp  9.3-main.pid          .s.PGSQL.5433         .s.PGSQL.5436.lock  .s.PGSQL.5440       .s.PGSQL.5443.lock&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;(Yeah I run a lot of versions. :-)&lt;/p&gt;

&lt;p&gt;This is one way to fix the problem:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;postgres@tal:~$ PGCLUSTER=14/main psql -p 5443
psql (17devel)
Type "help" for help.

postgres=#&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But that’s too annoying, and the &lt;code&gt;\d&lt;/code&gt; commands are going to be broken because they won’t know how to query the latest &lt;code&gt;pg_*&lt;/code&gt; tables. (And by the way, why does psql still say it’s &lt;code&gt;17devel&lt;/code&gt;? I haven’t looked into that yet but it’s suspicious.&lt;sup&gt;1&lt;/sup&gt;) And in fact even using &lt;code&gt;PGCLUSTER=30/main psql&lt;/code&gt; still works!&lt;/p&gt;

&lt;p&gt;I think it’s a bug in this Perl code from &lt;code&gt;/usr/bin/psql&lt;/code&gt;:&lt;/p&gt;

&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-perl"&gt;# if only a port is specified, look for local cluster on specified port
if ($explicit_port and not $version and not $cluster and not $explicit_host and not $explicit_service) {
    LOOP: foreach my $v (reverse get_versions()) {
        foreach my $c (get_version_clusters $v) {
            my $p = get_cluster_port $v, $c;
            if ($p eq $explicit_port) {
                $version = $v;
                # set PGCLUSTER variable for information
                $ENV{PGCLUSTER} = "$version/$c";
                last LOOP;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can see that it sets &lt;code&gt;$version&lt;/code&gt; but not &lt;code&gt;$cluster&lt;/code&gt; (just &lt;code&gt;$ENV{PGCLUSTER}&lt;/code&gt;). Later if &lt;code&gt;$cluster&lt;/code&gt; is set then it will look up the correct socket dir, but it’s only set if we’re explicit. Personally I’m fixing this by adding &lt;code&gt;$cluster = $c;&lt;/code&gt; right before the &lt;code&gt;$version = $v&lt;/code&gt; line. Then we’ll call &lt;code&gt;get_cluster_socketdir&lt;/code&gt; below. It might not be 100% correct but it is good enough for my purposes.&lt;/p&gt;

&lt;p&gt;So now I have a custom-patched Postgres running on Ubuntu! I see its &lt;code&gt;/etc&lt;/code&gt; files, its data files, and its log file. After &lt;code&gt;systemctl daemon-reload&lt;/code&gt; I can start it etc. So I think I’m all set. I’d just better re-run &lt;code&gt;./configure --prefix=~/local&lt;/code&gt; before I forget and re-install something broken on top of it. :-)&lt;/p&gt;

&lt;p&gt;If I run into more problems, I’ll update this post.&lt;/p&gt;
&lt;hr style="border-bottom: 1px solid #999; width: 50%"&gt;&lt;p&gt;&lt;sup&gt;1&lt;/sup&gt; Oh, the answer is simple. From &lt;code&gt;/usr/bin/psql&lt;/code&gt;:&lt;/p&gt;
&lt;div class="CodeRay"&gt;&lt;div class="code"&gt;&lt;pre&gt;&lt;code class="language-perl"&gt;# if we have no version yet, use the latest version. If we were called as psql,
# pg_archivecleanup, or pg_isready, always use latest version
if (not $version or $cmdname =~ /^(psql|pg_archivecleanup|pg_isready)$/) {
    my $max_version;
    if ($version and $version &amp;lt; 9.2) { # psql 15 only supports PG 9.2+
        $max_version = 14;
    }
    $version = get_newest_version($cmdname, $max_version);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But that means most of the last paragraph was wrong. Since the non-self-compiled tools find the socket file just fine, there must be a better solution than patching &lt;code&gt;psql&lt;/code&gt; (which is technically &lt;code&gt;pg_wrapper&lt;/code&gt; btw). So we are not done. Stay tuned for the, ahem, sequel!&lt;/p&gt;
</content>
  </entry>
</feed>

