Adding SHOW CREATE TABLE

Started by Nathaniel Sabanskiover 2 years ago46 messages
#1Nathaniel Sabanski
sabanski.n@gmail.com

HN had a thread regarding the challenges faced by new users during the
adoption of Postgres in 2023.

One particular issue that garnered significant votes was the lack of a
"SHOW CREATE TABLE" command, and seems like it would be an easy one to
implement: https://news.ycombinator.com/item?id=35908991

Considering the popularity of this request and its potential ease of
implementation, I wanted to bring it to your attention, as it would likely
enhance the user experience and alleviate some of the difficulties
encountered by newcomers.

#2Stephen Frost
sfrost@snowman.net
In reply to: Nathaniel Sabanski (#1)
Re: Adding SHOW CREATE TABLE

Greetings,

* Nathaniel Sabanski (sabanski.n@gmail.com) wrote:

HN had a thread regarding the challenges faced by new users during the
adoption of Postgres in 2023.

One particular issue that garnered significant votes was the lack of a
"SHOW CREATE TABLE" command, and seems like it would be an easy one to
implement: https://news.ycombinator.com/item?id=35908991

Considering the popularity of this request and its potential ease of
implementation, I wanted to bring it to your attention, as it would likely
enhance the user experience and alleviate some of the difficulties
encountered by newcomers.

This isn't as easy as it seems actually ...

Note that using pg_dump for this purpose works quite well and also works
to address cross-version issues. Consider that pg_dump v15 is able to
connect to v14, v13, v12, v11, and more, and produce a CREATE TABLE
command that will work with *v15*. If you connected to a v14 database
and did a SHOW CREATE TABLE, there's no guarantee that the CREATE TABLE
statement returned would work for PG v15 due to keyword changes and
other differences that can cause issues between major versions of PG.

Now, that said, we have started ending up with some similar code between
pg_dump and postgres_fdw in the form of IMPORT FOREIGN SCHEMA and maybe
we should consider if that code could be moved into the common library
and made available to pg_dump, postgres_fdw, and as a SHOW CREATE TABLE
command with the caveat that the produced CREATE TABLE command may not
work with newer versions of PG. There's an interesting question around
if we'd consider it a bug worthy of fixing if IMPORT FOREIGN SCHEMA in
v14 doesn't work when connecting to a v15 PG instance. Not sure if
anyone's contemplated that. There's certainly going to be cases that we
wouldn't accept fixing (we wouldn't add some new partitioning strategy
to v14 just because it's in v15, for example, to make IMPORT FOREIGN
SCHEMA work...).

Thanks,

Stephen

#3Nathaniel Sabanski
sabanski.n@gmail.com
In reply to: Stephen Frost (#2)
Re: Adding SHOW CREATE TABLE

I believe most users would anticipate a CREATE TABLE statement that aligns
with the currently installed version- this is the practical solution for
the vast majority.

In situations where a CREATE TABLE statement compatible with an older
version of Postgres is required, users can opt for an additional step of
using tools like pg_dump or an older version of Postgres itself. This
allows them to ensure compatibility without compromising the practicality
of the process.

On Fri, 12 May 2023 at 06:47, Stephen Frost <sfrost@snowman.net> wrote:

Show quoted text

Greetings,

* Nathaniel Sabanski (sabanski.n@gmail.com) wrote:

HN had a thread regarding the challenges faced by new users during the
adoption of Postgres in 2023.

One particular issue that garnered significant votes was the lack of a
"SHOW CREATE TABLE" command, and seems like it would be an easy one to
implement: https://news.ycombinator.com/item?id=35908991

Considering the popularity of this request and its potential ease of
implementation, I wanted to bring it to your attention, as it would

likely

enhance the user experience and alleviate some of the difficulties
encountered by newcomers.

This isn't as easy as it seems actually ...

Note that using pg_dump for this purpose works quite well and also works
to address cross-version issues. Consider that pg_dump v15 is able to
connect to v14, v13, v12, v11, and more, and produce a CREATE TABLE
command that will work with *v15*. If you connected to a v14 database
and did a SHOW CREATE TABLE, there's no guarantee that the CREATE TABLE
statement returned would work for PG v15 due to keyword changes and
other differences that can cause issues between major versions of PG.

Now, that said, we have started ending up with some similar code between
pg_dump and postgres_fdw in the form of IMPORT FOREIGN SCHEMA and maybe
we should consider if that code could be moved into the common library
and made available to pg_dump, postgres_fdw, and as a SHOW CREATE TABLE
command with the caveat that the produced CREATE TABLE command may not
work with newer versions of PG. There's an interesting question around
if we'd consider it a bug worthy of fixing if IMPORT FOREIGN SCHEMA in
v14 doesn't work when connecting to a v15 PG instance. Not sure if
anyone's contemplated that. There's certainly going to be cases that we
wouldn't accept fixing (we wouldn't add some new partitioning strategy
to v14 just because it's in v15, for example, to make IMPORT FOREIGN
SCHEMA work...).

Thanks,

Stephen

#4Stephen Frost
sfrost@snowman.net
In reply to: Nathaniel Sabanski (#3)
Re: Adding SHOW CREATE TABLE

Greetings,

Please don't top-post on these lists.

* Nathaniel Sabanski (sabanski.n@gmail.com) wrote:

I believe most users would anticipate a CREATE TABLE statement that aligns
with the currently installed version- this is the practical solution for
the vast majority.

Perhaps a bit more discussion about what exactly the use-case is would
be helpful- what would you use this feature for?

In situations where a CREATE TABLE statement compatible with an older
version of Postgres is required, users can opt for an additional step of
using tools like pg_dump or an older version of Postgres itself. This
allows them to ensure compatibility without compromising the practicality
of the process.

The issue is really both older and newer versions, not just older ones
and not just newer ones.

To the extent you're interested in this, I pointed out where you could
go look at the existing code as well as an idea for how to move this
forward.

Thanks,

Stephen

#5Thorsten Glaser
tg@evolvis.org
In reply to: Nathaniel Sabanski (#3)
Re: Adding SHOW CREATE TABLE

On Fri, 12 May 2023, Nathaniel Sabanski wrote:

I believe most users would anticipate a CREATE TABLE statement that aligns
with the currently installed version- this is the practical solution for

The currently installed version of what, the server or the client?

bye,
//mirabilos
--
15:41⎜<Lo-lan-do:#fusionforge> Somebody write a testsuite for helloworld :-)

#6Thomas Kellerer
shammat@gmx.net
In reply to: Nathaniel Sabanski (#1)
Re: Adding SHOW CREATE TABLE

Nathaniel Sabanski schrieb am 12.05.2023 um 13:29:

HN had a thread regarding the challenges faced by new users during
the adoption of Postgres in 2023.

One particular issue that garnered significant votes was the lack of
a "SHOW CREATE TABLE" command, and seems like it would be an easy one
to implement: https://news.ycombinator.com/item?id=35908991

Considering the popularity of this request and its potential ease of
implementation, I wanted to bring it to your attention, as it would
likely enhance the user experience and alleviate some of the
difficulties encountered by newcomers.

While it would be nice to have something like that, I don't think
it isn't really necessary. Pretty much every (GUI) SQL client provides
a way to see the DDL for objects in the database.

For psql fans \d typically is enough, and they would probably not mind
running pg_dump to get the full DDL.

I would think that especially newcomers start with a GUI client
that can do this.

If you check the source of any of the popular SQL clients that generates
the DDL for a table, then you will also quickly realize that this isn't
a trivial thing to do.

Thomas

#7David G. Johnston
david.g.johnston@gmail.com
In reply to: Thorsten Glaser (#5)
Re: Adding SHOW CREATE TABLE

On Fri, May 12, 2023, 08:35 Thorsten Glaser <tg@evolvis.org> wrote:

On Fri, 12 May 2023, Nathaniel Sabanski wrote:

I believe most users would anticipate a CREATE TABLE statement that aligns
with the currently installed version- this is the practical solution for

The currently installed version of what, the server or the client?

It's an SQL Command, no specific client can/should be presumed.

David J.

Show quoted text
#8Nathaniel Sabanski
sabanski.n@gmail.com
In reply to: David G. Johnston (#7)
Re: Adding SHOW CREATE TABLE

On Fri, 12 May 2023 at 09:12, David G. Johnston <david.g.johnston@gmail.com>
wrote:

Show quoted text

On Fri, May 12, 2023, 08:35 Thorsten Glaser <tg@evolvis.org> wrote:

On Fri, 12 May 2023, Nathaniel Sabanski wrote:

I believe most users would anticipate a CREATE TABLE statement that

aligns

with the currently installed version- this is the practical solution for

The currently installed version of what, the server or the client?

It's an SQL Command, no specific client can/should be presumed.

David J.

#9Nathaniel Sabanski
sabanski.n@gmail.com
In reply to: Nathaniel Sabanski (#8)
Re: Adding SHOW CREATE TABLE

Perhaps a bit more discussion about what exactly the use-case is would
be helpful- what would you use this feature for?

App writers: To facilitate table creation and simplify schema verification,
without relying on a GUI tool or ORM (or system calls out to pg_dump).

Tool writers: Would drastically cut down the implementation time and
complexity to support Postgres. I am one of the devs of Piccolo ORM (Python
lib supporting Postgres) and we have a lot of code dedicated to
re-generating the CREATE TABLE statements (creation, during migrations,
etc) that could be done better by Postgres itself.

Ecosystem cohesion: SHOW CREATE TABLE has already been implemented in
CockroachDB, a popular Postgres derivative.

Moving to Postgres: It would help ease migrations for developers wanting to
move from MySQL / Percona / MariaDB to Postgres. Also it's a nice developer
experience to see how Postgres generates X table without extra tooling.

The intention of SHOW CREATE TABLE is not to replace the existing suite of
\d in psql but rather to be a developer friendly complement within SQL
itself.

#10Stephen Frost
sfrost@snowman.net
In reply to: Nathaniel Sabanski (#9)
Re: Adding SHOW CREATE TABLE

Greetings,

* Nathaniel Sabanski (sabanski.n@gmail.com) wrote:

Perhaps a bit more discussion about what exactly the use-case is would
be helpful- what would you use this feature for?

App writers: To facilitate table creation and simplify schema verification,
without relying on a GUI tool or ORM (or system calls out to pg_dump).

Not sure how it would simplify schema verification?

Tool writers: Would drastically cut down the implementation time and
complexity to support Postgres. I am one of the devs of Piccolo ORM (Python
lib supporting Postgres) and we have a lot of code dedicated to
re-generating the CREATE TABLE statements (creation, during migrations,
etc) that could be done better by Postgres itself.

I'm curious- have you compared what you're doing to pg_dump's output?
Are you confident that there aren't any distinctions between those that,
for whatever reason, need to exist?

Moving to Postgres: It would help ease migrations for developers wanting to
move from MySQL / Percona / MariaDB to Postgres. Also it's a nice developer
experience to see how Postgres generates X table without extra tooling.

Seems unlikely that this would actually be all that helpful there- tools
like ora2pg and similar know how to query other database systems and
write appropriate CREATE TABLE statements for PostgreSQL.

The intention of SHOW CREATE TABLE is not to replace the existing suite of
\d in psql but rather to be a developer friendly complement within SQL
itself.

Sure, I get that.

Again, would be great to see someone actually work on this. There's
already a good chunk of code in core in pg_dump and in the postgres_fdw
for doing exactly this and it'd be great to consolidate that and at the
same time expose it via SQL.

Another possible option would be to add this to libpq, which is used by
postgres_fdw, psql, pg_dump, and lots of other drivers and client
utilities. If it's all broadly the same, personally I'd prefer it to be
in the common library and available as a backend SQL command too, but
perhaps there's reasons that it would be easier to implement in libpq
instead.

Thanks,

Stephen

#11Tom Lane
tgl@sss.pgh.pa.us
In reply to: Stephen Frost (#10)
Re: Adding SHOW CREATE TABLE

Stephen Frost <sfrost@snowman.net> writes:

Again, would be great to see someone actually work on this. There's
already a good chunk of code in core in pg_dump and in the postgres_fdw
for doing exactly this and it'd be great to consolidate that and at the
same time expose it via SQL.

Note that this is hardly new ground: we've heard more-or-less the same
proposal many times before. I think the reason it's gone nowhere is
that most of the existing infrastructure is either in pg_dump or designed
to support pg_dump, and pg_dump is *extremely* opinionated about what
it wants and how it wants the data sliced up, for very good reasons.
Reconciling those requirements with a typical user's "just give me a
reconstructed CREATE TABLE command" request seems fairly difficult.

Also, since pg_dump will still need to support old servers, it's hard
to believe we'd accept any proposal to move that functionality into
the server side, which in turn means that it's not going to be an easy
SQL command.

These issues probably could be surmounted with enough hard work, but
please understand that just coming along with a request is not going
to cause it to happen. People have already done that. (Searching
the mailing list archives might be edifying.)

regards, tom lane

#12Stephen Frost
sfrost@snowman.net
In reply to: Tom Lane (#11)
Re: Adding SHOW CREATE TABLE

Greetings,

* Tom Lane (tgl@sss.pgh.pa.us) wrote:

Stephen Frost <sfrost@snowman.net> writes:

Again, would be great to see someone actually work on this. There's
already a good chunk of code in core in pg_dump and in the postgres_fdw
for doing exactly this and it'd be great to consolidate that and at the
same time expose it via SQL.

Note that this is hardly new ground: we've heard more-or-less the same
proposal many times before. I think the reason it's gone nowhere is
that most of the existing infrastructure is either in pg_dump or designed
to support pg_dump, and pg_dump is *extremely* opinionated about what
it wants and how it wants the data sliced up, for very good reasons.
Reconciling those requirements with a typical user's "just give me a
reconstructed CREATE TABLE command" request seems fairly difficult.

Yet we're already duplicating much of this in postgres_fdw. If we don't
want to get involved in pg_dump's feelings on the subject, we could look
to postgres_fdw's independent implementation which might be more
in-line with what users are expecting. Having two separate copies of
code that does this and continuing to refuse to give users a way to ask
for it themselves seems at the least like an odd choice.

Also, since pg_dump will still need to support old servers, it's hard
to believe we'd accept any proposal to move that functionality into
the server side, which in turn means that it's not going to be an easy
SQL command.

No, it won't make sense to have yet another copy that's for the
currently-running-server-only, which is why I suggested it go into
either a common library or maybe into libpq. I don't feel it would
be bad for the common code to have the multi-version understanding even
if the currently running backend will only ever have the option to ask
for the code path that matches its version.

These issues probably could be surmounted with enough hard work, but
please understand that just coming along with a request is not going
to cause it to happen. People have already done that. (Searching
the mailing list archives might be edifying.)

Agreed- someone needs to have a fair bit of time and willingness to push
on this to make it happen.

Thanks,

Stephen

#13Kirk Wolak
wolakk@gmail.com
In reply to: Stephen Frost (#12)
Re: Adding SHOW CREATE TABLE

On Fri, May 12, 2023 at 4:37 PM Stephen Frost <sfrost@snowman.net> wrote:

Greetings,

* Tom Lane (tgl@sss.pgh.pa.us) wrote:

Stephen Frost <sfrost@snowman.net> writes:

Again, would be great to see someone actually work on this. There's
already a good chunk of code in core in pg_dump and in the postgres_fdw
for doing exactly this and it'd be great to consolidate that and at the
same time expose it via SQL.

...
No, it won't make sense to have yet another copy that's for the
currently-running-server-only, which is why I suggested it go into
either a common library or maybe into libpq. I don't feel it would
be bad for the common code to have the multi-version understanding even
if the currently running backend will only ever have the option to ask
for the code path that matches its version.

Hmmm... What's wrong with only being for the currently running server?

That's all I would expect. Also, if it was there, it limits the
expectations to DDL that
works for that server version.

Also, if it's on the backend (or an extension), then it's available to
everything.

Agreed- someone needs to have a fair bit of time and willingness to push
on this to make it happen.

If we can work through a CLEAR discussion of what it is, and is not. I
would be
happy to work on this. I like referencing the FDW. I also thought of
referencing
the CREATE TABLE xyz(LIKE abc INCLUDING ALL). While it's not doing DDL,
it certainly has to be checking options, etc. And pg_dump is the "gold
standard".

My approach would be to get a version working. Then figure out how to
generate "literally" all table options, and work the process. The good news
is that at a certain point the resulting DDL should be "comparable" against
a
ton of test tables.

Where do we draw the lines? Does Table DDL include all indexes?
It should include constraints, clearly. I would not think it should have
triggers.
Literally everything within the <<CREATE TABLE X(...);>>. (ie, no ALTER ..
OWNER TO...)

Next, I would want psql \st to simply call this?

FWIW, we parse our pg_dump output, and store the objects as individual DDL
files.
So, I have about 1,000 tables to play with, for which I already know the
DDL that pg_dump uses.

But it's a big commitment. I don't mind if it has a reasonable chance of
being accepted.
I accept that I will make a few mistakes (and learn) along the way.
If there are ANY deal killers that would prevent a reasonable solution from
being accepted,
please let me know.

Kirk...

#14Stephen Frost
sfrost@snowman.net
In reply to: Kirk Wolak (#13)
Re: Adding SHOW CREATE TABLE

Greetings,

* Kirk Wolak (wolakk@gmail.com) wrote:

On Fri, May 12, 2023 at 4:37 PM Stephen Frost <sfrost@snowman.net> wrote:

* Tom Lane (tgl@sss.pgh.pa.us) wrote:

Stephen Frost <sfrost@snowman.net> writes:

Again, would be great to see someone actually work on this. There's
already a good chunk of code in core in pg_dump and in the postgres_fdw
for doing exactly this and it'd be great to consolidate that and at the
same time expose it via SQL.

...
No, it won't make sense to have yet another copy that's for the
currently-running-server-only, which is why I suggested it go into
either a common library or maybe into libpq. I don't feel it would
be bad for the common code to have the multi-version understanding even
if the currently running backend will only ever have the option to ask
for the code path that matches its version.

Hmmm... What's wrong with only being for the currently running server?
That's all I would expect. Also, if it was there, it limits the
expectations to DDL that
works for that server version.

I didn't say anything was wrong with that, merely pointing out that
having the same set of code for these various use-cases would be better
than having multiple copies of it. The existing code works just fine to
answer the question of "when on v15, what is the v15 query?", it just
happens to *also* answer "when on v15, what is the v14 query?" and we
need that already for postgres_fdw and for pg_dump.

Also, if it's on the backend (or an extension), then it's available to
everything.

I mean ... it's already in postgres_fdw, just not in a way that can be
returned to the user. I don't think I'd want this functionality to
depend on postgres_fdw or generally on an extension though, it should
be part of core in some fashion.

Agreed- someone needs to have a fair bit of time and willingness to push
on this to make it happen.

If we can work through a CLEAR discussion of what it is, and is not. I
would be
happy to work on this. I like referencing the FDW. I also thought of
referencing
the CREATE TABLE xyz(LIKE abc INCLUDING ALL). While it's not doing DDL,
it certainly has to be checking options, etc. And pg_dump is the "gold
standard".

I'd think the FDW code would be the best starting point, but, sure, look
at all the options.

My approach would be to get a version working. Then figure out how to
generate "literally" all table options, and work the process. The good news
is that at a certain point the resulting DDL should be "comparable" against
a ton of test tables.

Where do we draw the lines? Does Table DDL include all indexes?
It should include constraints, clearly. I would not think it should have
triggers.
Literally everything within the <<CREATE TABLE X(...);>>. (ie, no ALTER ..
OWNER TO...)

I'd look at the IMPORT FOREIGN SCHEMA stuff in postgres_fdw. We're
already largely answering these questions by what options that takes.
To some extent, the same is true of pg_dump, but at least postgres_fdw
is already backend code and probably a bit simpler than the pg_dump
code. Still, looking at both would be a good idea.

Next, I would want psql \st to simply call this?

Eh, that's an independent discussion and effort, especially because
people are possibly going to want that to generate the necessary ALTER
TABLE commands from the result and not just a DROP/CREATE TABLE.

FWIW, we parse our pg_dump output, and store the objects as individual DDL
files.
So, I have about 1,000 tables to play with, for which I already know the
DDL that pg_dump uses.

Sure.

But it's a big commitment. I don't mind if it has a reasonable chance of
being accepted.

Yes, it's a large effort, no doubt.

I accept that I will make a few mistakes (and learn) along the way.
If there are ANY deal killers that would prevent a reasonable solution from
being accepted, please let me know.

I don't think we can say one way or the other on this ...

Thanks,

Stephen

#15Ron
ronljohnsonjr@gmail.com
In reply to: Kirk Wolak (#13)
Re: Adding SHOW CREATE TABLE

On 5/12/23 18:00, Kirk Wolak wrote:
[snip]

Where do we draw the lines?

At other tables.

Does Table DDL include all indexes?

Absolutely!

It should include constraints, clearly.  I would not think it should have
triggers.

Definitely triggers.  And foreign keys.

Literally everything within the <<CREATE TABLE X(...);>>.  (ie, no ALTER
.. OWNER TO...)

ALTER statements, too.  If CREATE TABLE ... LIKE ... { INCLUDING | EXCLUDING
} { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY |
INDEXES | STATISTICS | STORAGE | ALL } can do it, then so should SHOW CREATE
TABLE.

--
Born in Arizona, moved to Babylonia.

#16Kirk Wolak
wolakk@gmail.com
In reply to: Ron (#15)
Re: Adding SHOW CREATE TABLE

On Sat, May 13, 2023 at 1:03 AM Ron <ronljohnsonjr@gmail.com> wrote:

On 5/12/23 18:00, Kirk Wolak wrote:

[snip]

Where do we draw the lines?

At other tables.

Does Table DDL include all indexes?

Absolutely!

It should include constraints, clearly. I would not think it should have
triggers.

Definitely triggers. And foreign keys.

Literally everything within the <<CREATE TABLE X(...);>>. (ie, no ALTER
.. OWNER TO...)

ALTER statements, too. If CREATE TABLE ... LIKE ... { INCLUDING |
EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED |
IDENTITY | INDEXES | STATISTICS | STORAGE | ALL } can do it, then so should
SHOW CREATE TABLE.

--
Born in Arizona, moved to Babylonia.

I can see the ALTER statements now. Which is why I asked.
I don't like the idea of including the trigger DDL, because that would
never execute in a clean environment.
(I've never used a tool that tried to do that when I've wanted the DDL)
I can go either way on index creation.

Does this imply SQL SYNTAX like:

SHOW CREATE TABLE <table_name>
[ INCLUDING { ALL | INDEXES | SEQUENCES | ??? }]
[EXCLUDING { PK | FK | COMMENTS | STORAGE | } ]
[FOR {V11 | V12 | V13 | V14 | V15 }] ??
?

The goal for me is to open the discussion, and then CONSTRAIN the focus.

Personally, the simple syntax:
SHOW CREATE TABLE table1;

Should give me a create table command with the table attributes and the
column attributes, FKs, PKs, Defaults. Etc.
But I would not expect it to generate index commands, etc.

#17Kirk Wolak
wolakk@gmail.com
In reply to: Stephen Frost (#14)
Re: Adding SHOW CREATE TABLE

On Fri, May 12, 2023 at 8:37 PM Stephen Frost <sfrost@snowman.net> wrote:

Greetings,
..
I mean ... it's already in postgres_fdw, just not in a way that can be
returned to the user. I don't think I'd want this functionality to
depend on postgres_fdw or generally on an extension though, it should
be part of core in some fashion.

I will start with postgres_fdw then, but gladly review the other source...
Just thinking about the essence of the syntax.
SHOW CREATE TABLE abc(LIKE real_table); -- Output CREATE TABLE abc();
using real_table?

I'd look at the IMPORT FOREIGN SCHEMA stuff in postgres_fdw. We're

already largely answering these questions by what options that takes.

Will do.

But it's a big commitment. I don't mind if it has a reasonable chance of
being accepted.

Yes, it's a large effort, no doubt.

At least there is a base of code to start with.
I see a strong need to come up with a shell script to that could:

FOR <every schema.table> DO
psql -c "SHOW... \g | cat > <schema.table.DDL> "
pg_dump -- <schema.table> only | remove_comments_normalize | cat
<schema.table.pg_dump>
DIFF <schema.table.DDL> <schema.table.pg_dump>

Of course, since our tests are usually all perl, a perl version.
But I would clearly want some heavy testing/validation.

#18Ron
ronljohnsonjr@gmail.com
In reply to: Kirk Wolak (#16)
Re: Adding SHOW CREATE TABLE

On 5/13/23 02:25, Kirk Wolak wrote:

On Sat, May 13, 2023 at 1:03 AM Ron <ronljohnsonjr@gmail.com> wrote:

On 5/12/23 18:00, Kirk Wolak wrote:
[snip]

Where do we draw the lines?

At other tables.

Does Table DDL include all indexes?

Absolutely!

It should include constraints, clearly.  I would not think it should
have triggers.

Definitely triggers.  And foreign keys.

Literally everything within the <<CREATE TABLE X(...);>>.  (ie, no
ALTER .. OWNER TO...)

ALTER statements, too.  If CREATE TABLE ... LIKE ... { INCLUDING |
EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS |
GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL } can do
it, then so should SHOW CREATE TABLE.

--
Born in Arizona, moved to Babylonia.

I can see the ALTER statements now.  Which is why I asked.
I don't like the idea of including the trigger DDL, because that would
never execute in a clean environment.

I would not be grumpy if trigger statements weren't included.

(I've never used a tool that tried to do that when I've wanted the DDL)
I can go either way on index creation.

Does this imply SQL SYNTAX like:

SHOW CREATE TABLE <table_name>
  [ INCLUDING { ALL | INDEXES |  SEQUENCES | ??? }]
  [EXCLUDING { PK | FK | COMMENTS | STORAGE | } ]
  [FOR {V11 | V12 | V13 | V14 | V15 }] ??
?

"FOR {V...}" is a complication too far, IMO.  No one expects "pg_dump
--schema-only" to have a --version= option, so one should not expect SHOW
CREATE TABLE to have a "FOR {V...}" clause.

The goal for me  is to open the discussion, and then CONSTRAIN the focus.

Personally, the simple syntax:
SHOW CREATE TABLE table1;

Should give me a create table command with the table attributes and the
column attributes, FKs, PKs, Defaults. Etc.
But I would not expect it to generate index commands, etc.

--
Born in Arizona, moved to Babylonia.

#19Jeremy Smith
jeremy@musicsmith.net
In reply to: Kirk Wolak (#16)
Re: Adding SHOW CREATE TABLE

On Sat, May 13, 2023, 3:25 AM Kirk Wolak <wolakk@gmail.com> wrote:

Does this imply SQL SYNTAX like:

SHOW CREATE TABLE <table_name>
[ INCLUDING { ALL | INDEXES | SEQUENCES | ??? }]
[EXCLUDING { PK | FK | COMMENTS | STORAGE | } ]
[FOR {V11 | V12 | V13 | V14 | V15 }] ??
?

Personally, I would expect a function, like pg_get_tabledef(oid), to match
the other pg_get_*def functions instead of overloading SHOW. To me, this
also argues that we shouldn't include indexes because we already have a
pg_get_indexdef function.

-Jeremy

Show quoted text
#20Kirk Wolak
wolakk@gmail.com
In reply to: Jeremy Smith (#19)
Re: Adding SHOW CREATE TABLE

On Sat, May 13, 2023 at 3:34 PM Jeremy Smith <jeremy@musicsmith.net> wrote:

On Sat, May 13, 2023, 3:25 AM Kirk Wolak <wolakk@gmail.com> wrote:

Does this imply SQL SYNTAX like:

SHOW CREATE TABLE <table_name>
[ INCLUDING { ALL | INDEXES | SEQUENCES | ??? }]
[EXCLUDING { PK | FK | COMMENTS | STORAGE | } ]
[FOR {V11 | V12 | V13 | V14 | V15 }] ??
?

Personally, I would expect a function, like pg_get_tabledef(oid), to match
the other pg_get_*def functions instead of overloading SHOW. To me, this
also argues that we shouldn't include indexes because we already have a
pg_get_indexdef function.

-Jeremy

+1

In fact, making it a function will make my life easier for testing, that's
for certain. I don't need to involve the parser,etc. Others can help with
that after the function works.
Thanks for the suggestion!

#21Kirk Wolak
wolakk@gmail.com
In reply to: Stephen Frost (#14)
Re: Adding SHOW CREATE TABLE

On Fri, May 12, 2023 at 8:37 PM Stephen Frost <sfrost@snowman.net> wrote:

Greetings,
...
Yes, it's a large effort, no doubt.

Stephen, I started looking at the code.
And I have the queries from \set SHOW_HIDDEN
that psql uses. And also the pg_dump output.

My first table was an ID bigint NOT NULL PRIMARY KEY GENERATED ALWAYS AS
IDENTITY

pg_dump puts the decorations on the SEQUENCE
\dt puts that text as the "Default" value

But the STRANGE part for me is the query I Assembled from the FDW returns
nothing for extra attributes.
And only seems to care about the "GENERATED AS (%s) STORED" syntax.

For me, generating the INLINE syntax will produce the SEQUENCE
automatically, so this is my preference.
Let me know if I am missing anything... Please.

Finally, I cannot GRASP this additional syntax:
appendStringInfo(&buf, "\n) SERVER %s\nOPTIONS (",

This is at the end of the create table syntax:
CREATE TABLE %s ( ... ) SERVER %s\n OPTIONS (" ...");

Is this special "FDW" Decorations because I don't see those on the create
table documentation?
It's easy enough to ignore, but I don't want to miss something.

Kirk...

#22Kirk Wolak
wolakk@gmail.com
In reply to: Kirk Wolak (#20)
Re: Adding SHOW CREATE TABLE

On Sun, May 14, 2023 at 2:20 AM Kirk Wolak <wolakk@gmail.com> wrote:

On Sat, May 13, 2023 at 3:34 PM Jeremy Smith <jeremy@musicsmith.net>
wrote:

On Sat, May 13, 2023, 3:25 AM Kirk Wolak <wolakk@gmail.com> wrote:

Does this imply SQL SYNTAX like:

SHOW CREATE TABLE <table_name>
[ INCLUDING { ALL | INDEXES | SEQUENCES | ??? }]
[EXCLUDING { PK | FK | COMMENTS | STORAGE | } ]
[FOR {V11 | V12 | V13 | V14 | V15 }] ??
?

Personally, I would expect a function, like pg_get_tabledef(oid), to
match the other pg_get_*def functions instead of overloading SHOW. To me,
this also argues that we shouldn't include indexes because we already have
a pg_get_indexdef function.

-Jeremy

+1

In fact, making it a function will make my life easier for testing, that's
for certain. I don't need to involve the parser,etc. Others can help with
that after the function works.
Thanks for the suggestion!

I am moving this over to the Hackers Group.
My approach for now is to develop this as the \st command.
After reviewing the code/output from the 3 sources (psql, fdw, and
pg_dump). This trivializes the approach,
and requires the smallest set of changes (psql is already close with
existing queries, etc).

And frankly, I would rather have an \st feature that handles 99% of the use
cases then go 15yrs waiting for a perfect solution.
Once this works well for the group. Then, IMO, that would be the time to
discuss moving it.

Support Or Objections Appreciated.
Kirk...

#23Stephen Frost
sfrost@snowman.net
In reply to: Kirk Wolak (#22)
Re: Adding SHOW CREATE TABLE

Greetings,

* Kirk Wolak (wolakk@gmail.com) wrote:

My approach for now is to develop this as the \st command.
After reviewing the code/output from the 3 sources (psql, fdw, and
pg_dump). This trivializes the approach,
and requires the smallest set of changes (psql is already close with
existing queries, etc).

And frankly, I would rather have an \st feature that handles 99% of the use
cases then go 15yrs waiting for a perfect solution.
Once this works well for the group. Then, IMO, that would be the time to
discuss moving it.

Having this only available via psql seems like the least desirable
option as then it wouldn't be available to any other callers..

Having it in libpq, on the other hand, would make it available to psql
as well as any other utility, library, or language / driver which uses
libpq, including pg_dump..

Using libpq would also make sense from the perspective that libpq can be
used to connect to a number of different major versions of PG and this
could work work for them all in much the way that pg_dump does.

The downside with this apporach is that drivers which don't use libpq
(eg: JDBC) would have to re-implement this if they wanted to keep
feature parity with libpq..

Thanks,

Stephen

#24Andrew Dunstan
andrew@dunslane.net
In reply to: Stephen Frost (#23)
Re: Adding SHOW CREATE TABLE

On 2023-05-18 Th 19:53, Stephen Frost wrote:

Greetings,

* Kirk Wolak (wolakk@gmail.com) wrote:

My approach for now is to develop this as the \st command.
After reviewing the code/output from the 3 sources (psql, fdw, and
pg_dump). This trivializes the approach,
and requires the smallest set of changes (psql is already close with
existing queries, etc).

And frankly, I would rather have an \st feature that handles 99% of the use
cases then go 15yrs waiting for a perfect solution.
Once this works well for the group. Then, IMO, that would be the time to
discuss moving it.

Having this only available via psql seems like the least desirable
option as then it wouldn't be available to any other callers..

Having it in libpq, on the other hand, would make it available to psql
as well as any other utility, library, or language / driver which uses
libpq, including pg_dump..

Using libpq would also make sense from the perspective that libpq can be
used to connect to a number of different major versions of PG and this
could work work for them all in much the way that pg_dump does.

The downside with this apporach is that drivers which don't use libpq
(eg: JDBC) would have to re-implement this if they wanted to keep
feature parity with libpq..

I think the ONLY place we should have this is in server side functions.
More than ten years ago I did some work in this area (see below), but
it's one of those things that have been on my ever growing personal TODO
list

See <https://bitbucket.org/adunstan/retailddl/src/master/&gt; and
<https://www.youtube.com/watch?v=fBarFKOL3SI&gt;

cheers

andrew

--
Andrew Dunstan
EDB:https://www.enterprisedb.com

#25Corey Huinker
corey.huinker@gmail.com
In reply to: Andrew Dunstan (#24)
Re: Adding SHOW CREATE TABLE

I think the ONLY place we should have this is in server side functions.
More than ten years ago I did some work in this area (see below), but it's
one of those things that have been on my ever growing personal TODO list

See <https://bitbucket.org/adunstan/retailddl/src/master/&gt;
<https://bitbucket.org/adunstan/retailddl/src/master/&gt; and
<https://www.youtube.com/watch?v=fBarFKOL3SI&gt;
<https://www.youtube.com/watch?v=fBarFKOL3SI&gt;

Some additional backstory, as this has come up before...

A direction discussion of a previous SHOW CREATE effort:
/messages/by-id/20190705163203.GD24679@fetter.org

Other databases' implementations of SHOW CREATE came up in this discussion
as well:
/messages/by-id/CADkLM=fxfsrHASKk_bY_A4uomJ1Te5MfGgD_rwwQfV8wP68ewg@mail.gmail.com

Clearly, there is customer demand for something like this, and has been for
a long time.

#26Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Andrew Dunstan (#24)
Re: Adding SHOW CREATE TABLE

On Fri, 2023-05-19 at 13:08 -0400, Andrew Dunstan wrote:

On 2023-05-18 Th 19:53, Stephen Frost wrote:

* Kirk Wolak (wolakk@gmail.com) wrote:

My approach for now is to develop this as the \st command.
After reviewing the code/output from the 3 sources (psql, fdw, and
pg_dump).

Having this only available via psql seems like the least desirable
option as then it wouldn't be available to any other callers..

Having it in libpq, on the other hand, would make it available to psql
as well as any other utility, library, or language / driver which uses
libpq, including pg_dump..

I think the ONLY place we should have this is in server side functions.

+1

A function "pg_get_tabledef" would blend nicely into the following list:

\df pg_get_*def
List of functions
Schema │ Name │ Result data type │ Argument data types │ Type
════════════╪════════════════════════════════╪══════════════════╪═══════════════════════╪══════
pg_catalog │ pg_get_constraintdef │ text │ oid │ func
pg_catalog │ pg_get_constraintdef │ text │ oid, boolean │ func
pg_catalog │ pg_get_functiondef │ text │ oid │ func
pg_catalog │ pg_get_indexdef │ text │ oid │ func
pg_catalog │ pg_get_indexdef │ text │ oid, integer, boolean │ func
pg_catalog │ pg_get_partition_constraintdef │ text │ oid │ func
pg_catalog │ pg_get_partkeydef │ text │ oid │ func
pg_catalog │ pg_get_ruledef │ text │ oid │ func
pg_catalog │ pg_get_ruledef │ text │ oid, boolean │ func
pg_catalog │ pg_get_statisticsobjdef │ text │ oid │ func
pg_catalog │ pg_get_triggerdef │ text │ oid │ func
pg_catalog │ pg_get_triggerdef │ text │ oid, boolean │ func
pg_catalog │ pg_get_viewdef │ text │ oid │ func
pg_catalog │ pg_get_viewdef │ text │ oid, boolean │ func
pg_catalog │ pg_get_viewdef │ text │ oid, integer │ func
pg_catalog │ pg_get_viewdef │ text │ text │ func
pg_catalog │ pg_get_viewdef │ text │ text, boolean │ func
(17 rows)

A server function can be conveniently called from any client code.

Yours,
Laurenz Albe

#27Ziga
ziga@ljudmila.org
In reply to: Andrew Dunstan (#24)
Re: Adding SHOW CREATE TABLE

On 19/05/2023 19:08, Andrew Dunstan wrote:

I think the ONLY place we should have this is in server side
functions. More than ten years ago I did some work in this area (see
below), but it's one of those things that have been on my ever growing
personal TODO list

See <https://bitbucket.org/adunstan/retailddl/src/master/&gt; and
<https://www.youtube.com/watch?v=fBarFKOL3SI&gt;

I have my own implementation of SHOW CREATE as a ddlx extension
available at https://github.com/lacanoid/pgddl

Far from perfect but getting better and it seems good enough to be used
by some people...

Ž.

#28Stephen Frost
sfrost@snowman.net
In reply to: Laurenz Albe (#26)
Re: Adding SHOW CREATE TABLE

Greetings,

* Laurenz Albe (laurenz.albe@cybertec.at) wrote:

On Fri, 2023-05-19 at 13:08 -0400, Andrew Dunstan wrote:

On 2023-05-18 Th 19:53, Stephen Frost wrote:

* Kirk Wolak (wolakk@gmail.com) wrote:

My approach for now is to develop this as the \st command.
After reviewing the code/output from the 3 sources (psql, fdw, and
pg_dump).

Having this only available via psql seems like the least desirable
option as then it wouldn't be available to any other callers..

Having it in libpq, on the other hand, would make it available to psql
as well as any other utility, library, or language / driver which uses
libpq, including pg_dump..

I think the ONLY place we should have this is in server side functions.

+1

... but it already exists in pg_dump, so I'm unclear why folks seem to
be pushing to have a duplicate of it in core? We certainly can't remove
it from pg_dump even if we add it to core because pg_dump has to
understand the changes between major versions.

A function "pg_get_tabledef" would blend nicely into the following list:

\df pg_get_*def
List of functions
Schema │ Name │ Result data type │ Argument data types │ Type
════════════╪════════════════════════════════╪══════════════════╪═══════════════════════╪══════
pg_catalog │ pg_get_constraintdef │ text │ oid │ func
pg_catalog │ pg_get_constraintdef │ text │ oid, boolean │ func
pg_catalog │ pg_get_functiondef │ text │ oid │ func
pg_catalog │ pg_get_indexdef │ text │ oid │ func
pg_catalog │ pg_get_indexdef │ text │ oid, integer, boolean │ func
pg_catalog │ pg_get_partition_constraintdef │ text │ oid │ func
pg_catalog │ pg_get_partkeydef │ text │ oid │ func
pg_catalog │ pg_get_ruledef │ text │ oid │ func
pg_catalog │ pg_get_ruledef │ text │ oid, boolean │ func
pg_catalog │ pg_get_statisticsobjdef │ text │ oid │ func
pg_catalog │ pg_get_triggerdef │ text │ oid │ func
pg_catalog │ pg_get_triggerdef │ text │ oid, boolean │ func
pg_catalog │ pg_get_viewdef │ text │ oid │ func
pg_catalog │ pg_get_viewdef │ text │ oid, boolean │ func
pg_catalog │ pg_get_viewdef │ text │ oid, integer │ func
pg_catalog │ pg_get_viewdef │ text │ text │ func
pg_catalog │ pg_get_viewdef │ text │ text, boolean │ func
(17 rows)

A server function can be conveniently called from any client code.

Clearly any client using libpq can conveniently call code which is in
libpq.

Thanks,

Stephen

#29David G. Johnston
david.g.johnston@gmail.com
In reply to: Stephen Frost (#28)
Re: Adding SHOW CREATE TABLE

On Sat, May 20, 2023 at 10:26 AM Stephen Frost <sfrost@snowman.net> wrote:

A server function can be conveniently called from any client code.

Clearly any client using libpq can conveniently call code which is in
libpq.

Clearly there are clients that don't use libpq. JDBC comes to mind.

David J.

#30Tom Lane
tgl@sss.pgh.pa.us
In reply to: David G. Johnston (#29)
Re: Adding SHOW CREATE TABLE

"David G. Johnston" <david.g.johnston@gmail.com> writes:

On Sat, May 20, 2023 at 10:26 AM Stephen Frost <sfrost@snowman.net> wrote:

Clearly any client using libpq can conveniently call code which is in
libpq.

Clearly there are clients that don't use libpq. JDBC comes to mind.

Yeah. I'm also rather concerned about the bloat factor; it's
hardly unlikely that this line of development would double the
size of libpq, to add functionality that only a small minority
of applications would use. A client-side implementation also has
no choice but to cope with multiple server versions, and to
figure out what it's going to do about a too-new server.
Up to now we broke compatibility between libpq and server maybe
once every couple decades, but it'd be once a year for this.

regards, tom lane

#31Stephen Frost
sfrost@snowman.net
In reply to: David G. Johnston (#29)
Re: Adding SHOW CREATE TABLE

Greetings,

On Sat, May 20, 2023 at 13:32 David G. Johnston <david.g.johnston@gmail.com>
wrote:

On Sat, May 20, 2023 at 10:26 AM Stephen Frost <sfrost@snowman.net> wrote:

A server function can be conveniently called from any client code.

Clearly any client using libpq can conveniently call code which is in
libpq.

Clearly there are clients that don't use libpq. JDBC comes to mind.

Indeed … as I mentioned up-thread already.

Are we saying that we want this to be available server side, and largely
duplicated, specifically to cater to non-libpq users? I’ll put out there,
again, the idea that perhaps we put it into the common library then and
make it available via both libpq and as a server side function ..?

We also have similar code in postgres_fdw.. ideally, imv anyway, we’d not
end up with three copies of it.

Thanks,

Stephen

#32Kirk Wolak
wolakk@gmail.com
In reply to: Stephen Frost (#31)
Re: Adding SHOW CREATE TABLE

On Sat, May 20, 2023 at 2:33 PM Stephen Frost <sfrost@snowman.net> wrote:

Greetings,

On Sat, May 20, 2023 at 13:32 David G. Johnston <
david.g.johnston@gmail.com> wrote:

On Sat, May 20, 2023 at 10:26 AM Stephen Frost <sfrost@snowman.net>
wrote:

A server function can be conveniently called from any client code.

Clearly any client using libpq can conveniently call code which is in
libpq.

Clearly there are clients that don't use libpq. JDBC comes to mind.

Indeed … as I mentioned up-thread already.

Are we saying that we want this to be available server side, and largely
duplicated, specifically to cater to non-libpq users? I’ll put out there,
again, the idea that perhaps we put it into the common library then and
make it available via both libpq and as a server side function ..?

We also have similar code in postgres_fdw.. ideally, imv anyway, we’d not
end up with three copies of it.

Thanks,

Stephen

First, as the person chasing this down, and a JDBC user, I really would
prefer pg_get_tabledef() as Laurenz mentioned.

Next, I have reviewed all 3 implementations (pg_dump [most appropriate],
psql \d (very similar), and the FDW which is "way off",
since it actually focuses on "CREATE FOREIGN TABLE" exclusively, and
already fails to handle many pieces not required in
creating a "real" table, as it creates a "reflection" of table.

I am using pg_dump as my source of truth. But I noticed it does not create
"TEMPORARY" tables with that syntax.
[Leading to a question on mutating the pg_temp_# schema name back to
pg_temp. or just stripping it, in favor of the TEMPORARY]

I was surprised to see ~ 2,000 lines of code in the FDW and in psql...
Whereas pg_dump is shorter because it gets more detailed
table information in a structure passed in.

I would love to leverage existing code, in the end. But I want to take my
time on this, and become intimate with the details.
Each of the above 3 approaches have different goals. And I would prefer
the lowest risk:reward possible, and the least expensive
maintenance. Having it run server side hides a ton of details, and as Tom
pointed out, obviates DDL versioning control for other server versions.

Thanks for the references to the old discussions. I have queued them up to
review.

Kirk...

#33Kirk Wolak
wolakk@gmail.com
In reply to: Andrew Dunstan (#24)
Re: Adding SHOW CREATE TABLE

On Fri, May 19, 2023 at 1:08 PM Andrew Dunstan <andrew@dunslane.net> wrote:

I think the ONLY place we should have this is in server side functions.
More than ten years ago I did some work in this area (see below), but it's
one of those things that have been on my ever growing personal TODO list

See <https://bitbucket.org/adunstan/retailddl/src/master/&gt;
<https://bitbucket.org/adunstan/retailddl/src/master/&gt; and
<https://www.youtube.com/watch?v=fBarFKOL3SI&gt;
<https://www.youtube.com/watch?v=fBarFKOL3SI&gt;

Andrew,
Thanks for sharing that. I reviewed your code. 10yrs, clearly it's not
working (as-is, but close), something interesting about the
structure you ended up in. You check the type of the object and redirect
accordingly at the top level. Hmmm...
What I liked was that each type gets handled (I was focused on "table"),
but I realized similarities.

I don't know what the group would think, but I like the thought of
calling this, and having it "Correct" to call the appropriate function.
But not sure it will stand. It does make obvious that some of these should
be spun out as "pg_get_typedef"..
pg_get_typedef
pg_get_domaindef
pg_get_sequencedef

Finally, since you started this a while back, part of me is "leaning"
towards a function:
pg_get_columndef

Which returns a properly formatted column for a table, type, or domain?
(one of the reasons for this, is that this is
the function with the highest probability to change, and potentially the
easiest to share reusability).

Finally, I am curious about your opinion. I noticed you used the
internal pg_ tables, versus the information_schema...
I am *thinking* that the information_schema will be more stable over
time... Thoughts?

Thank you for sharing your thoughts...
Kirk...

#34Pavel Stehule
pavel.stehule@gmail.com
In reply to: Kirk Wolak (#33)
Re: Adding SHOW CREATE TABLE

po 22. 5. 2023 v 7:19 odesílatel Kirk Wolak <wolakk@gmail.com> napsal:

On Fri, May 19, 2023 at 1:08 PM Andrew Dunstan <andrew@dunslane.net>
wrote:

I think the ONLY place we should have this is in server side functions.
More than ten years ago I did some work in this area (see below), but it's
one of those things that have been on my ever growing personal TODO list

See <https://bitbucket.org/adunstan/retailddl/src/master/&gt;
<https://bitbucket.org/adunstan/retailddl/src/master/&gt; and
<https://www.youtube.com/watch?v=fBarFKOL3SI&gt;
<https://www.youtube.com/watch?v=fBarFKOL3SI&gt;

Andrew,
Thanks for sharing that. I reviewed your code. 10yrs, clearly it's not
working (as-is, but close), something interesting about the
structure you ended up in. You check the type of the object and redirect
accordingly at the top level. Hmmm...
What I liked was that each type gets handled (I was focused on "table"),
but I realized similarities.

I don't know what the group would think, but I like the thought of
calling this, and having it "Correct" to call the appropriate function.
But not sure it will stand. It does make obvious that some of these
should be spun out as "pg_get_typedef"..
pg_get_typedef
pg_get_domaindef
pg_get_sequencedef

Finally, since you started this a while back, part of me is "leaning"
towards a function:
pg_get_columndef

Which returns a properly formatted column for a table, type, or domain?
(one of the reasons for this, is that this is
the function with the highest probability to change, and potentially the
easiest to share reusability).

Finally, I am curious about your opinion. I noticed you used the
internal pg_ tables, versus the information_schema...
I am *thinking* that the information_schema will be more stable over
time... Thoughts?

I think inside the core, the information schema is never used. And there
was a performance issue (fixed in PostgreSQL 12), that blocked index usage.

Regards

Pavel

Show quoted text

Thank you for sharing your thoughts...
Kirk...

#35Andrew Dunstan
andrew@dunslane.net
In reply to: Pavel Stehule (#34)
Re: Adding SHOW CREATE TABLE

On 2023-05-22 Mo 05:24, Pavel Stehule wrote:

po 22. 5. 2023 v 7:19 odesílatel Kirk Wolak <wolakk@gmail.com> napsal:

On Fri, May 19, 2023 at 1:08 PM Andrew Dunstan
<andrew@dunslane.net> wrote:

I think the ONLY place we should have this is in server side
functions. More than ten years ago I did some work in this
area (see below), but it's one of those things that have been
on my ever growing personal TODO list

See <https://bitbucket.org/adunstan/retailddl/src/master/&gt;
<https://bitbucket.org/adunstan/retailddl/src/master/&gt; and
<https://www.youtube.com/watch?v=fBarFKOL3SI&gt;
<https://www.youtube.com/watch?v=fBarFKOL3SI&gt;

Andrew,
  Thanks for sharing that.  I reviewed your code. 10yrs, clearly
it's not working (as-is, but close), something interesting about the
structure you ended up in.  You check the type of the object and
redirect accordingly at the top level. Hmmm...
What I liked was that each type gets handled (I was focused on
"table"), but I realized similarities.

  I don't know what the group would think, but I like the thought
of calling this, and having it "Correct" to call the appropriate
function.
But not sure it will stand.  It does make obvious that some of
these should be spun out as "pg_get_typedef"..
pg_get_typedef
pg_get_domaindef
pg_get_sequencedef

  Finally, since you started this a while back, part of me is
"leaning" towards a function:
pg_get_columndef

  Which returns a properly formatted column for a table, type, or
domain? (one of the reasons for this, is that this is
the function with the highest probability to change, and
potentially the easiest to share reusability).

  Finally, I am curious about your opinion.  I noticed you used
the internal pg_ tables, versus the information_schema...
I am *thinking* that the information_schema will be more stable
over time... Thoughts?

I think inside the core, the information schema is never used.  And
there was a performance issue (fixed in PostgreSQL 12), that blocked
index usage.

A performant server side set of functions would be written in C and
follow the patterns in ruleutils.c.

cheers

andrew

--
Andrew Dunstan
EDB:https://www.enterprisedb.com

#36Jelte Fennema
postgres@jeltef.nl
In reply to: Andrew Dunstan (#35)
Re: Adding SHOW CREATE TABLE

On Mon, 22 May 2023 at 13:52, Andrew Dunstan <andrew@dunslane.net> wrote:

A performant server side set of functions would be written in C and follow the patterns in ruleutils.c.

We have lots of DDL ruleutils in our Citus codebase:
https://github.com/citusdata/citus/blob/main/src/backend/distributed/deparser/citus_ruleutils.c

I'm pretty sure we'd be happy to upstream those if that meant, we
wouldn't have to update them for every postgres release.

We also have the master_get_table_ddl_events UDF, which does what SHOW
CREATE TABLE would do.

#37Andrew Dunstan
andrew@dunslane.net
In reply to: Jelte Fennema (#36)
Re: Adding SHOW CREATE TABLE

On 2023-05-25 Th 09:23, Jelte Fennema wrote:

On Mon, 22 May 2023 at 13:52, Andrew Dunstan<andrew@dunslane.net> wrote:

A performant server side set of functions would be written in C and follow the patterns in ruleutils.c.

We have lots of DDL ruleutils in our Citus codebase:
https://github.com/citusdata/citus/blob/main/src/backend/distributed/deparser/citus_ruleutils.c

I'm pretty sure we'd be happy to upstream those if that meant, we
wouldn't have to update them for every postgres release.

We also have the master_get_table_ddl_events UDF, which does what SHOW
CREATE TABLE would do.

Sounds promising. I'd love to see a patch.

cheers

andrew

--
Andrew Dunstan
EDB:https://www.enterprisedb.com

#38Kirk Wolak
wolakk@gmail.com
In reply to: Jelte Fennema (#36)
Re: Adding SHOW CREATE TABLE

On Thu, May 25, 2023 at 9:23 AM Jelte Fennema <postgres@jeltef.nl> wrote:

On Mon, 22 May 2023 at 13:52, Andrew Dunstan <andrew@dunslane.net> wrote:

A performant server side set of functions would be written in C and

follow the patterns in ruleutils.c.

We have lots of DDL ruleutils in our Citus codebase:

https://github.com/citusdata/citus/blob/main/src/backend/distributed/deparser/citus_ruleutils.c

I'm pretty sure we'd be happy to upstream those if that meant, we
wouldn't have to update them for every postgres release.

We also have the master_get_table_ddl_events UDF, which does what SHOW
CREATE TABLE would do.

Jelte, this looks promising, although it is a radically different approach
(Querying from it to get the details).

I was just getting ready to write up a bit of an RFC... On the following
approach...

I have been trying to determine how to "focus" this effort to move it
forward. Here is where I am at:
1) It should be 100% server side (and psql \st would only work by calling
the server side code, if it was there)
In reviewing... This simplifies the implementation to the current
version of PG DDL being generated.
Also, as others have mentioned, it should be C based code, and use
only the internal tables.
2) Since pg_get_{ triggerdef | indexdef | constraintdef } already exists, I
was strongly recommending to not include those.
-- Although including the inlined constraints would be fine by me
(potentially a boolean to turn it off?)
3) Then focusing the reloptions WITH (%s)

It appears CITUS code handles ALL of this on a cursory review!

The ONLY thing I did not see was "CREATE TEMPORARY " syntax? If you did
this on a TEMP table,
does it generate normal table syntax or TEMPORARY TABLE syntax???

So, from my take... This is a great example of solving the problem with
existing "Production Quality" Code...
I like it...

Can this get turned into a Patch? Were you offering this code up for
others (me?) to pull, and work into a patch?
[If I do the patch, I am not sure it gives you the value of reducing what
CITUS has to maintain. But it dawns on
me that you might be pushing a much bigger patch... But I would take that,
as I think there is other value in there]

Others???

Thanks,

Kirk...
PS: It dawned on me that if pg_dump had used server side code to generate
its DDL, its complexity would drop.

#39Andrew Dunstan
andrew@dunslane.net
In reply to: Kirk Wolak (#38)
Re: Adding SHOW CREATE TABLE

On 2023-06-01 Th 12:57, Kirk Wolak wrote:

PS: It dawned on me that if pg_dump had used server side code to
generate its DDL, its complexity would drop.

Maybe, that remains to be seen. pg_dump needs to produce SQL that is
suitable for the target database, not the source database.

cheers

andrew

--
Andrew Dunstan
EDB:https://www.enterprisedb.com

#40Kirk Wolak
wolakk@gmail.com
In reply to: Andrew Dunstan (#39)
Re: Adding SHOW CREATE TABLE

On Thu, Jun 1, 2023 at 3:13 PM Andrew Dunstan <andrew@dunslane.net> wrote:

On 2023-06-01 Th 12:57, Kirk Wolak wrote:

PS: It dawned on me that if pg_dump had used server side code to generate
its DDL, its complexity would drop.

Maybe, that remains to be seen. pg_dump needs to produce SQL that is
suitable for the target database, not the source database.

First, pg_dump has some special needs in addressing how it creates tables,
to be able to load the data BEFORE indexing, and constraining (lest you
have to struggle with dependencies of FKs, etc etc)...

But version checking is interesting... Because I found no way to tell
pg_dump what DB to target. The code I did see was checking what server
options were available. (I clearly could have missed something)... But
exactly how would that work? All it knows is who it is (pg_dump V14, for
example. So the best case is that it ignores new things that would be in
V15, V16 because it doesn't even know what to check for or what to do).
Which, to me, makes it more of a side-effect than a feature, no?

And that if it used a server side solution, it gets an interesting "super
power". In that even pg_dump V20 could dump a V21 db with V21 syntax
enhancements. (I cannot say if that is desirable or not as I lack 20yrs of
history here). Finally, the thoughts of implementing this change at this
point, when it would impact anyone using the NEW version to dump an OLD
version without the server side support... So that migration may never
happen. Or only after the only supported DBs have the required server side
functions...

The feedback is welcome...

#41Andrew Dunstan
andrew@dunslane.net
In reply to: Kirk Wolak (#40)
Re: Adding SHOW CREATE TABLE

On 2023-06-01 Th 16:39, Kirk Wolak wrote:

On Thu, Jun 1, 2023 at 3:13 PM Andrew Dunstan <andrew@dunslane.net> wrote:

On 2023-06-01 Th 12:57, Kirk Wolak wrote:

PS: It dawned on me that if pg_dump had used server side code to
generate its DDL, its complexity would drop.

Maybe, that remains to be seen. pg_dump needs to produce SQL that
is suitable for the target database, not the source database.

First, pg_dump has some special needs in addressing how it creates
tables, to be able to load the data BEFORE indexing, and constraining
(lest you have to struggle with dependencies of FKs, etc etc)...

But version checking is interesting... Because I found no way to tell
pg_dump what DB to target.

The target version is implicitly the version it's built from.

cheers

andrew

--
Andrew Dunstan
EDB:https://www.enterprisedb.com

#42Kirk Wolak
wolakk@gmail.com
In reply to: Andrew Dunstan (#41)
Re: Adding SHOW CREATE TABLE

On Thu, Jun 1, 2023 at 4:46 PM Andrew Dunstan <andrew@dunslane.net> wrote:

On 2023-06-01 Th 16:39, Kirk Wolak wrote:

On Thu, Jun 1, 2023 at 3:13 PM Andrew Dunstan <andrew@dunslane.net> wrote:

On 2023-06-01 Th 12:57, Kirk Wolak wrote:

PS: It dawned on me that if pg_dump had used server side code to generate
its DDL, its complexity would drop.

Maybe, that remains to be seen. pg_dump needs to produce SQL that is
suitable for the target database, not the source database.

First, pg_dump has some special needs in addressing how it creates tables,
to be able to load the data BEFORE indexing, and constraining (lest you
have to struggle with dependencies of FKs, etc etc)...

But version checking is interesting... Because I found no way to tell
pg_dump what DB to target.

The target version is implicitly the version it's built from.

Andrew,
Thank you. Someone else confirmed for me that the code is designed to
create accurate DDL for the pg_dump version.
So, for example, WITH OIDs are not included with later versions of pg_dump,
even though they are hitting a server with them.
Great to know.

I like that CITUS offered up something. I think that might be the
current best approach. It's a win-win. They get something,
we get something.

Kirk...

#43Jelte Fennema
postgres@jeltef.nl
In reply to: Kirk Wolak (#38)
1 attachment(s)
Re: Adding SHOW CREATE TABLE

On Thu, 1 Jun 2023 at 18:57, Kirk Wolak <wolakk@gmail.com> wrote:

Can this get turned into a Patch? Were you offering this code up for others (me?) to pull, and work into a patch?
[If I do the patch, I am not sure it gives you the value of reducing what CITUS has to maintain. But it dawns on
me that you might be pushing a much bigger patch... But I would take that, as I think there is other value in there]

Attached is a patch which adds the relevant functions from Citus to
Postgres (it compiles without warnings on my machine). I checked with
a few others on the Citus team at Microsoft and everyone thought that
upstreaming this was a good idea, because it's quite a bit of work to
update with every postgres release.

To set expectations though, I don't really have time to work on this
patch. So if you can take it over from here that would be great.

The patch only contains the C functions which generate SQL based on
some oids. The wrappers such as the master_get_table_ddl_events
function were too hard for me to pull out of Citus code, because they
integrated a lot with other pieces. But the bulk of the logic is in
the functions in this patch. Essentially all that
master_get_table_ddl_events does is call the functions in this patch
in the right order.

The ONLY thing I did not see was "CREATE TEMPORARY " syntax? If you did this on a TEMP table,
does it generate normal table syntax or TEMPORARY TABLE syntax???

Yeah, the Citus code only handles things that Citus supports in
distributed tables. Which is quite a lot, but indeed not everything
yet. Temporary and inherited tables are not supported in this code
afaik. Possibly more. See the commented out
EnsureRelationKindSupported for what should be supported (normal
tables and partitioned tables afaik).

Attachments:

v1-0001-Add-initial-code-to-generate-table-definition-SQL.patchapplication/octet-stream; name=v1-0001-Add-initial-code-to-generate-table-definition-SQL.patchDownload
From ddb375afc74339bd0eaf0c272d06805637fd85cc Mon Sep 17 00:00:00 2001
From: Jelte Fennema <jelte.fennema@microsoft.com>
Date: Mon, 5 Jun 2023 12:32:12 +0200
Subject: [PATCH v1] Add initial code to generate table definition SQL

This patch adds various functions which generate table DDL statements
based on a table oid. These functions originated in Citus source code,
but are now contributed to upstream Postgres by means of this patch.
It currently contains no wrappers around these C functions that can be
called from SQL.
---
 src/backend/utils/adt/acl.c       |    2 +-
 src/backend/utils/adt/ruleutils.c | 1187 ++++++++++++++++++++++++++++-
 src/include/utils/acl.h           |    1 +
 src/include/utils/ruleutils.h     |   51 ++
 src/tools/pgindent/typedefs.list  |    2 +
 5 files changed, 1240 insertions(+), 3 deletions(-)

diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index c660fd3e701..abe329d4c83 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -1702,7 +1702,7 @@ convert_any_priv_string(text *priv_type_text,
 }
 
 
-static const char *
+const char *
 convert_aclright_to_string(int aclright)
 {
 	switch (aclright)
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index d3a973d86b7..30bb4e7ffd9 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -24,12 +24,16 @@
 #include "access/relation.h"
 #include "access/sysattr.h"
 #include "access/table.h"
+#include "access/toast_compression.h"
 #include "catalog/pg_aggregate.h"
 #include "catalog/pg_am.h"
+#include "catalog/pg_attrdef.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
+#include "catalog/pg_extension.h"
+#include "catalog/pg_foreign_data_wrapper.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
@@ -39,10 +43,13 @@
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
+#include "commands/extension.h"
+#include "commands/sequence.h"
 #include "commands/tablespace.h"
 #include "common/keywords.h"
 #include "executor/spi.h"
 #include "funcapi.h"
+#include "foreign/foreign.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -59,6 +66,7 @@
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rewriteSupport.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
@@ -507,7 +515,6 @@ static Node *processIndirection(Node *node, deparse_context *context);
 static void printSubscripts(SubscriptingRef *sbsref, deparse_context *context);
 static char *get_relation_name(Oid relid);
 static char *generate_relation_name(Oid relid, List *namespaces);
-static char *generate_qualified_relation_name(Oid relid);
 static char *generate_function_name(Oid funcid, int nargs,
 									List *argnames, Oid *argtypes,
 									bool has_variadic, bool *use_variadic_p,
@@ -521,6 +528,10 @@ static void get_reloptions(StringInfo buf, Datum reloptions);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
+#define CREATE_SEQUENCE_COMMAND \
+	"CREATE %sSEQUENCE IF NOT EXISTS %s AS %s INCREMENT BY " INT64_FORMAT \
+	" MINVALUE " INT64_FORMAT " MAXVALUE " INT64_FORMAT \
+	" START WITH " INT64_FORMAT " CACHE " INT64_FORMAT " %sCYCLE"
 
 /* ----------
  * pg_get_ruledef		- Do it all and return a text
@@ -12110,7 +12121,7 @@ generate_relation_name(Oid relid, List *namespaces)
  *
  * As above, but unconditionally schema-qualify the name.
  */
-static char *
+char *
 generate_qualified_relation_name(Oid relid)
 {
 	HeapTuple	tp;
@@ -12606,3 +12617,1175 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
+ * pg_get_extensiondef_string finds the foreign data wrapper that corresponds to
+ * the given foreign tableId, and checks if an extension owns this foreign data
+ * wrapper. If it does, the function returns the extension's definition. If not,
+ * the function returns null.
+ */
+char *
+pg_get_extensiondef_string(Oid tableRelationId)
+{
+	ForeignTable *foreignTable = GetForeignTable(tableRelationId);
+	ForeignServer *server = GetForeignServer(foreignTable->serverid);
+	ForeignDataWrapper *foreignDataWrapper = GetForeignDataWrapper(server->fdwid);
+	StringInfoData buffer = {NULL, 0, 0, 0};
+
+	Oid			classId = ForeignDataWrapperRelationId;
+	Oid			objectId = server->fdwid;
+
+	Oid			extensionId = getExtensionOfObject(classId, objectId);
+
+	if (OidIsValid(extensionId))
+	{
+		char	   *extensionName = get_extension_name(extensionId);
+		Oid			extensionSchemaId = get_extension_schema(extensionId);
+		char	   *extensionSchema = get_namespace_name(extensionSchemaId);
+
+		initStringInfo(&buffer);
+		appendStringInfo(&buffer, "CREATE EXTENSION IF NOT EXISTS %s WITH SCHEMA %s",
+						 quote_identifier(extensionName),
+						 quote_identifier(extensionSchema));
+	}
+	else
+	{
+		ereport(NOTICE, (errmsg("foreign-data wrapper \"%s\" does not have an "
+								"extension defined", foreignDataWrapper->fdwname)));
+	}
+
+	return (buffer.data);
+}
+
+/*
+ * get_extension_version - given an extension OID, fetch its extversion
+ * or NULL if not found.
+ */
+char *
+get_extension_version(Oid extensionId)
+{
+	char	   *versionName = NULL;
+
+	Relation	relation = table_open(ExtensionRelationId, AccessShareLock);
+	SysScanDesc scanDesc;
+	HeapTuple	tuple;
+
+	ScanKeyData entry[1];
+
+	ScanKeyInit(&entry[0],
+				Anum_pg_extension_oid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(extensionId));
+
+	scanDesc = systable_beginscan(relation, ExtensionOidIndexId, true,
+								  NULL, 1, entry);
+
+	tuple = systable_getnext(scanDesc);
+
+	/* We assume that there can be at most one matching tuple */
+	if (HeapTupleIsValid(tuple))
+	{
+		bool		isNull = false;
+		Datum		versionDatum = heap_getattr(tuple, Anum_pg_extension_extversion,
+												RelationGetDescr(relation), &isNull);
+
+		if (!isNull)
+		{
+			versionName = text_to_cstring(DatumGetTextPP(versionDatum));
+		}
+	}
+
+	systable_endscan(scanDesc);
+
+	table_close(relation, AccessShareLock);
+
+	return versionName;
+}
+
+/*
+ * pg_get_serverdef_string finds the foreign server that corresponds to the
+ * given foreign tableId, and returns this server's definition.
+ */
+char *
+pg_get_serverdef_string(Oid tableRelationId)
+{
+	ForeignTable *foreignTable = GetForeignTable(tableRelationId);
+	ForeignServer *server = GetForeignServer(foreignTable->serverid);
+	ForeignDataWrapper *foreignDataWrapper = GetForeignDataWrapper(server->fdwid);
+
+	StringInfoData buffer = {NULL, 0, 0, 0};
+
+	initStringInfo(&buffer);
+
+	appendStringInfo(&buffer, "CREATE SERVER IF NOT EXISTS %s",
+					 quote_identifier(server->servername));
+	if (server->servertype != NULL)
+	{
+		appendStringInfo(&buffer, " TYPE %s",
+						 quote_literal_cstr(server->servertype));
+	}
+	if (server->serverversion != NULL)
+	{
+		appendStringInfo(&buffer, " VERSION %s",
+						 quote_literal_cstr(server->serverversion));
+	}
+
+	appendStringInfo(&buffer, " FOREIGN DATA WRAPPER %s",
+					 quote_identifier(foreignDataWrapper->fdwname));
+
+	/* append server options, if any */
+	AppendOptionListToString(&buffer, server->options);
+
+	return (buffer.data);
+}
+
+/*
+ * pg_get_sequencedef_string returns the definition of a given sequence. This
+ * definition includes explicit values for all CREATE SEQUENCE options.
+ */
+char *
+pg_get_sequencedef_string(Oid sequenceRelationId)
+{
+	Form_pg_sequence pgSequenceForm = pg_get_sequencedef(sequenceRelationId);
+
+	/* build our DDL command */
+	char	   *qualifiedSequenceName = generate_qualified_relation_name(sequenceRelationId);
+	char	   *typeName = format_type_be(pgSequenceForm->seqtypid);
+
+	char	   *sequenceDef = psprintf(CREATE_SEQUENCE_COMMAND,
+									   get_rel_persistence(sequenceRelationId) ==
+									   RELPERSISTENCE_UNLOGGED ? "UNLOGGED " : "",
+									   qualifiedSequenceName,
+									   typeName,
+									   pgSequenceForm->seqincrement, pgSequenceForm->seqmin,
+									   pgSequenceForm->seqmax, pgSequenceForm->seqstart,
+									   pgSequenceForm->seqcache,
+									   pgSequenceForm->seqcycle ? "" : "NO ");
+
+	return sequenceDef;
+}
+
+/*
+ * pg_get_sequencedef returns the Form_pg_sequence data about the sequence with the given
+ * object id.
+ */
+Form_pg_sequence
+pg_get_sequencedef(Oid sequenceRelationId)
+{
+	Form_pg_sequence pgSequenceForm;
+	HeapTuple	heapTuple = SearchSysCache1(SEQRELID, sequenceRelationId);
+
+	if (!HeapTupleIsValid(heapTuple))
+	{
+		elog(ERROR, "cache lookup failed for sequence %u", sequenceRelationId);
+	}
+
+	pgSequenceForm = (Form_pg_sequence) GETSTRUCT(heapTuple);
+
+	ReleaseSysCache(heapTuple);
+
+	return pgSequenceForm;
+}
+
+/*
+ * RegularTable function returns true if given table's relation kind is RELKIND_RELATION
+ * or RELKIND_PARTITIONED_TABLE otherwise it returns false.
+ */
+bool
+RegularTable(Oid relationId)
+{
+	char		relationKind = get_rel_relkind(relationId);
+
+	if (relationKind == RELKIND_RELATION || relationKind == RELKIND_PARTITIONED_TABLE)
+	{
+		return true;
+	}
+
+	return false;
+}
+
+/*
+ * pg_get_tableschemadef_string returns the definition of a given table. This
+ * definition includes table's schema, default column values, not null and check
+ * constraints. The definition does not include constraints that trigger index
+ * creations; specifically, unique and primary key constraints are excluded.
+ * When includeSequenceDefaults is INCLUDE_SEQUENCE_DEFAULTS, the function also creates
+ * DEFAULT clauses for columns getting their default values from a sequence.
+ * When IncludeIdentities is NO_IDENTITY, the function does not include identity column
+ * specifications. When it's INCLUDE_IDENTITY it creates GENERATED .. AS IDENTIY clauses.
+ */
+char *
+pg_get_tableschemadef_string(Oid tableRelationId,
+							 IncludeSequenceDefaults includeSequenceDefaults,
+							 IncludeIdentities includeIdentityDefaults,
+							 char *accessMethod)
+{
+	bool		firstAttributePrinted = false;
+	AttrNumber	defaultValueIndex = 0;
+	AttrNumber	constraintIndex = 0;
+	AttrNumber	constraintCount = 0;
+	StringInfoData buffer = {NULL, 0, 0, 0};
+
+	/*
+	 * Instead of retrieving values from system catalogs as other functions in
+	 * ruleutils.c do, we follow an unusual approach here: we open the
+	 * relation, and fetch the relation's tuple descriptor. We do this because
+	 * the tuple descriptor already contains information harnessed from
+	 * pg_attrdef, pg_attribute, pg_constraint, and pg_class; and therefore
+	 * using the descriptor saves us from a lot of additional work.
+	 */
+	Relation	relation = relation_open(tableRelationId, AccessShareLock);
+	char	   *relationName = generate_relation_name(tableRelationId, NIL);
+	char		relationKind;
+	TupleDesc	tupleDescriptor;
+	TupleConstr *tupleConstraints;
+
+	/* TODO: Actually implement functionality */
+	/* EnsureRelationKindSupported(tableRelationId); */
+
+	initStringInfo(&buffer);
+
+	if (RegularTable(tableRelationId))
+	{
+		appendStringInfoString(&buffer, "CREATE ");
+
+		if (relation->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED)
+		{
+			appendStringInfoString(&buffer, "UNLOGGED ");
+		}
+
+		appendStringInfo(&buffer, "TABLE %s (", relationName);
+	}
+	else
+	{
+		appendStringInfo(&buffer, "CREATE FOREIGN TABLE %s (", relationName);
+	}
+
+	/*
+	 * Iterate over the table's columns. If a particular column is not dropped
+	 * and is not inherited from another table, print the column's name and
+	 * its formatted type.
+	 */
+	tupleDescriptor = RelationGetDescr(relation);
+	tupleConstraints = tupleDescriptor->constr;
+
+	for (int attributeIndex = 0; attributeIndex < tupleDescriptor->natts;
+		 attributeIndex++)
+	{
+		Form_pg_attribute attributeForm = TupleDescAttr(tupleDescriptor, attributeIndex);
+
+		/*
+		 * We disregard the inherited attributes (i.e., attinhcount > 0) here.
+		 * The reasoning behind this is that Citus implements declarative
+		 * partitioning by creating the partitions first and then sending
+		 * "ALTER TABLE parent_table ATTACH PARTITION .." command. This may
+		 * not play well with regular inherited tables, which isn't a big
+		 * concern from Citus' perspective.
+		 */
+		if (!attributeForm->attisdropped)
+		{
+			const char *attributeName = NameStr(attributeForm->attname);
+
+			const char *attributeTypeName = format_type_with_typemod(
+																	 attributeForm->atttypid,
+																	 attributeForm->
+																	 atttypmod);
+
+			if (firstAttributePrinted)
+			{
+				appendStringInfoString(&buffer, ", ");
+			}
+			firstAttributePrinted = true;
+
+			appendStringInfo(&buffer, "%s ", quote_identifier(attributeName));
+			appendStringInfoString(&buffer, attributeTypeName);
+
+			if (CompressionMethodIsValid(attributeForm->attcompression))
+			{
+				appendStringInfo(&buffer, " COMPRESSION %s",
+								 GetCompressionMethodName(attributeForm->attcompression));
+			}
+
+			if (attributeForm->attidentity && includeIdentityDefaults)
+			{
+				bool		missing_ok = false;
+				Oid			seqOid = getIdentitySequence(RelationGetRelid(relation),
+														 attributeForm->attnum, missing_ok);
+
+				if (includeIdentityDefaults == INCLUDE_IDENTITY)
+				{
+					Form_pg_sequence pgSequenceForm = pg_get_sequencedef(seqOid);
+					char	   *sequenceDef = psprintf(
+													   " GENERATED %s AS IDENTITY (INCREMENT BY " INT64_FORMAT \
+													   " MINVALUE " INT64_FORMAT " MAXVALUE "
+													   INT64_FORMAT \
+													   " START WITH " INT64_FORMAT " CACHE "
+													   INT64_FORMAT " %sCYCLE)",
+													   attributeForm->attidentity == ATTRIBUTE_IDENTITY_ALWAYS ?
+													   "ALWAYS" : "BY DEFAULT",
+													   pgSequenceForm->seqincrement,
+													   pgSequenceForm->seqmin,
+													   pgSequenceForm->seqmax,
+													   pgSequenceForm->seqstart,
+													   pgSequenceForm->seqcache,
+													   pgSequenceForm->seqcycle ? "" : "NO ");
+
+					appendStringInfo(&buffer, "%s", sequenceDef);
+				}
+			}
+
+			/* if this column has a default value, append the default value */
+			if (attributeForm->atthasdef)
+			{
+				List	   *defaultContext = NULL;
+				char	   *defaultString = NULL;
+				AttrDefault *defaultValueList;
+				AttrDefault *defaultValue;
+				Node	   *defaultNode;
+
+				Assert(tupleConstraints != NULL);
+
+				defaultValueList = tupleConstraints->defval;
+				Assert(defaultValueList != NULL);
+
+				defaultValue = &(defaultValueList[defaultValueIndex]);
+				defaultValueIndex++;
+
+				Assert(defaultValue->adnum == (attributeIndex + 1));
+				Assert(defaultValueIndex <= tupleConstraints->num_defval);
+
+				/*
+				 * convert expression to node tree, and prepare deparse
+				 * context
+				 */
+				defaultNode = (Node *) stringToNode(defaultValue->adbin);
+
+				/*
+				 * if column default value is explicitly requested, or it is
+				 * not set from a sequence then we include DEFAULT clause for
+				 * this column.
+				 */
+				if (includeSequenceDefaults == INCLUDE_SEQUENCE_DEFAULTS ||
+					!contain_nextval_expression_walker(defaultNode, NULL))
+				{
+					defaultContext = deparse_context_for(relationName, tableRelationId);
+
+					/* deparse default value string */
+					defaultString = deparse_expression(defaultNode, defaultContext,
+													   false, false);
+
+					if (attributeForm->attgenerated == ATTRIBUTE_GENERATED_STORED)
+					{
+						appendStringInfo(&buffer, " GENERATED ALWAYS AS (%s) STORED",
+										 defaultString);
+					}
+					else
+					{
+						appendStringInfo(&buffer, " DEFAULT %s", defaultString);
+					}
+				}
+			}
+
+			/* if this column has a not null constraint, append the constraint */
+			if (attributeForm->attnotnull)
+			{
+				appendStringInfoString(&buffer, " NOT NULL");
+			}
+
+			if (attributeForm->attcollation != InvalidOid &&
+				attributeForm->attcollation != DEFAULT_COLLATION_OID)
+			{
+				appendStringInfo(&buffer, " COLLATE %s", generate_collation_name(
+																				 attributeForm->attcollation));
+			}
+		}
+	}
+
+	/*
+	 * Now check if the table has any constraints. If it does, set the number
+	 * of check constraints here. Then iterate over all check constraints and
+	 * print them.
+	 */
+	if (tupleConstraints != NULL)
+	{
+		constraintCount = tupleConstraints->num_check;
+	}
+
+	for (constraintIndex = 0; constraintIndex < constraintCount; constraintIndex++)
+	{
+		ConstrCheck *checkConstraintList = tupleConstraints->check;
+		ConstrCheck *checkConstraint = &(checkConstraintList[constraintIndex]);
+
+		/* convert expression to node tree, and prepare deparse context */
+		Node	   *checkNode = (Node *) stringToNode(checkConstraint->ccbin);
+		List	   *checkContext = deparse_context_for(relationName, tableRelationId);
+
+		/* deparse check constraint string */
+		char	   *checkString = deparse_expression(checkNode, checkContext, false, false);
+
+		/* if an attribute or constraint has been printed, format properly */
+		if (firstAttributePrinted || constraintIndex > 0)
+		{
+			appendStringInfoString(&buffer, ", ");
+		}
+
+		appendStringInfo(&buffer, "CONSTRAINT %s CHECK ",
+						 quote_identifier(checkConstraint->ccname));
+
+		appendStringInfoString(&buffer, "(");
+		appendStringInfoString(&buffer, checkString);
+		appendStringInfoString(&buffer, ")");
+	}
+
+	/* close create table's outer parentheses */
+	appendStringInfoString(&buffer, ")");
+
+	/*
+	 * If the relation is a foreign table, append the server name and options
+	 * to the create table statement.
+	 */
+	relationKind = relation->rd_rel->relkind;
+	if (relationKind == RELKIND_FOREIGN_TABLE)
+	{
+		ForeignTable *foreignTable = GetForeignTable(tableRelationId);
+		ForeignServer *foreignServer = GetForeignServer(foreignTable->serverid);
+
+		char	   *serverName = foreignServer->servername;
+
+		appendStringInfo(&buffer, " SERVER %s", quote_identifier(serverName));
+		AppendOptionListToString(&buffer, foreignTable->options);
+	}
+	else if (relationKind == RELKIND_PARTITIONED_TABLE)
+	{
+		char	   *partitioningInformation = GeneratePartitioningInformation(tableRelationId);
+
+		appendStringInfo(&buffer, " PARTITION BY %s ", partitioningInformation);
+	}
+
+	/*
+	 * Add table access methods when the table is configured with an access
+	 * method
+	 */
+	if (accessMethod)
+	{
+		appendStringInfo(&buffer, " USING %s", quote_identifier(accessMethod));
+	}
+	else if (OidIsValid(relation->rd_rel->relam))
+	{
+		Form_pg_am	amForm;
+		HeapTuple	amTup = SearchSysCache1(AMOID, ObjectIdGetDatum(
+																	relation->rd_rel->relam));
+
+		if (!HeapTupleIsValid(amTup))
+		{
+			elog(ERROR, "cache lookup failed for access method %u",
+				 relation->rd_rel->relam);
+		}
+		amForm = (Form_pg_am) GETSTRUCT(amTup);
+		appendStringInfo(&buffer, " USING %s", quote_identifier(NameStr(amForm->amname)));
+		ReleaseSysCache(amTup);
+	}
+
+	/*
+	 * Add any reloptions (storage parameters) defined on the table in a WITH
+	 * clause.
+	 */
+	{
+		char	   *reloptions = flatten_reloptions(tableRelationId);
+
+		if (reloptions)
+		{
+			appendStringInfo(&buffer, " WITH (%s)", reloptions);
+			pfree(reloptions);
+		}
+	}
+
+	relation_close(relation, AccessShareLock);
+
+	return (buffer.data);
+}
+
+/*
+ * EnsureRelationKindSupported errors out if the given relation is not supported
+ * as a distributed relation.
+ */
+/*
+ * void
+ * EnsureRelationKindSupported(Oid relationId)
+ * {
+ * 	char relationKind = get_rel_relkind(relationId);
+ * 	if (!relationKind)
+ * 	{
+ * 		ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ * 						errmsg("relation with OID %d does not exist",
+ * 							   relationId)));
+ * 	}
+ *
+ * 	bool supportedRelationKind = RegularTable(relationId) ||
+ * 								 relationKind == RELKIND_FOREIGN_TABLE;
+ *
+ * 	/\*
+ * 	 * Citus doesn't support bare inherited tables (i.e., not a partition or
+ * 	 * partitioned table)
+ * 	 *\/
+ * 	supportedRelationKind = supportedRelationKind && !(IsChildTable(relationId) ||
+ * 													   IsParentTable(relationId));
+ *
+ * 	if (!supportedRelationKind)
+ * 	{
+ * 		char *relationName = get_rel_name(relationId);
+ *
+ * 		ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ * 						errmsg("%s is not a regular, foreign or partitioned table",
+ * 							   relationName)));
+ * 	}
+ * }
+ */
+
+/*
+ * pg_get_tablecolumnoptionsdef_string returns column storage type and column
+ * statistics definitions for given table, _if_ these definitions differ from
+ * their default values. The function returns null if all columns use default
+ * values for their storage types and statistics.
+ */
+char *
+pg_get_tablecolumnoptionsdef_string(Oid tableRelationId)
+{
+	List	   *columnOptionList = NIL;
+	ListCell   *columnOptionCell = NULL;
+	bool		firstOptionPrinted = false;
+	StringInfoData buffer = {NULL, 0, 0, 0};
+
+	/*
+	 * Instead of retrieving values from system catalogs, we open the
+	 * relation, and use the relation's tuple descriptor to access attribute
+	 * information. This is primarily to maintain symmetry with
+	 * pg_get_tableschemadef.
+	 */
+	Relation	relation = relation_open(tableRelationId, AccessShareLock);
+
+	/* TODO: Actually implement functionality */
+	/* EnsureRelationKindSupported(tableRelationId); */
+
+	/*
+	 * Iterate over the table's columns. If a particular column is not dropped
+	 * and is not inherited from another table, check if column storage or
+	 * statistics statements need to be printed.
+	 */
+	TupleDesc	tupleDescriptor = RelationGetDescr(relation);
+	AttrNumber	natts;
+
+	if (tupleDescriptor->natts > MaxAttrNumber)
+	{
+		ereport(ERROR, (errmsg("bad number of tuple descriptor attributes")));
+	}
+
+	natts = tupleDescriptor->natts;
+	for (AttrNumber attributeIndex = 0;
+		 attributeIndex < natts;
+		 attributeIndex++)
+	{
+		Form_pg_attribute attributeForm = TupleDescAttr(tupleDescriptor, attributeIndex);
+		char	   *attributeName = NameStr(attributeForm->attname);
+		char		defaultStorageType = get_typstorage(attributeForm->atttypid);
+
+		if (!attributeForm->attisdropped && attributeForm->attinhcount == 0)
+		{
+			/*
+			 * If the user changed the column's default storage type, create
+			 * alter statement and add statement to a list for later
+			 * processing.
+			 */
+			if (attributeForm->attstorage != defaultStorageType)
+			{
+				char	   *storageName = 0;
+				StringInfoData statement = {NULL, 0, 0, 0};
+
+				initStringInfo(&statement);
+
+				switch (attributeForm->attstorage)
+				{
+					case 'p':
+						{
+							storageName = "PLAIN";
+							break;
+						}
+
+					case 'e':
+						{
+							storageName = "EXTERNAL";
+							break;
+						}
+
+					case 'm':
+						{
+							storageName = "MAIN";
+							break;
+						}
+
+					case 'x':
+						{
+							storageName = "EXTENDED";
+							break;
+						}
+
+					default:
+						{
+							ereport(ERROR, (errmsg("unrecognized storage type: %c",
+												   attributeForm->attstorage)));
+							break;
+						}
+				}
+
+				appendStringInfo(&statement, "ALTER COLUMN %s ",
+								 quote_identifier(attributeName));
+				appendStringInfo(&statement, "SET STORAGE %s", storageName);
+
+				columnOptionList = lappend(columnOptionList, statement.data);
+			}
+
+			/*
+			 * If the user changed the column's statistics target, create
+			 * alter statement and add statement to a list for later
+			 * processing.
+			 */
+			if (attributeForm->attstattarget >= 0)
+			{
+				StringInfoData statement = {NULL, 0, 0, 0};
+
+				initStringInfo(&statement);
+
+				appendStringInfo(&statement, "ALTER COLUMN %s ",
+								 quote_identifier(attributeName));
+				appendStringInfo(&statement, "SET STATISTICS %d",
+								 attributeForm->attstattarget);
+
+				columnOptionList = lappend(columnOptionList, statement.data);
+			}
+		}
+	}
+
+	/*
+	 * Iterate over column storage and statistics statements that we created,
+	 * and append them to a single alter table statement.
+	 */
+	foreach(columnOptionCell, columnOptionList)
+	{
+		char	   *columnOptionStatement = (char *) lfirst(columnOptionCell);
+
+		if (!firstOptionPrinted)
+		{
+			initStringInfo(&buffer);
+			appendStringInfo(&buffer, "ALTER TABLE ONLY %s ",
+							 generate_relation_name(tableRelationId, NIL));
+		}
+		else
+		{
+			appendStringInfoString(&buffer, ", ");
+		}
+		firstOptionPrinted = true;
+		appendStringInfoString(&buffer, columnOptionStatement);
+
+		pfree(columnOptionStatement);
+	}
+
+	list_free(columnOptionList);
+	relation_close(relation, AccessShareLock);
+
+	return (buffer.data);
+}
+
+/*
+ * pg_get_indexclusterdef_string returns the definition of a cluster statement
+ * for given index. The function returns null if the table is not clustered on
+ * given index.
+ */
+char *
+pg_get_indexclusterdef_string(Oid indexRelationId)
+{
+	StringInfoData buffer = {NULL, 0, 0, 0};
+	Form_pg_index indexForm;
+	Oid			tableRelationId;
+
+	HeapTuple	indexTuple = SearchSysCache(INDEXRELID, ObjectIdGetDatum(indexRelationId),
+											0, 0, 0);
+
+	if (!HeapTupleIsValid(indexTuple))
+	{
+		ereport(ERROR, (errmsg("cache lookup failed for index %u", indexRelationId)));
+	}
+
+	indexForm = (Form_pg_index) GETSTRUCT(indexTuple);
+	tableRelationId = indexForm->indrelid;
+
+	/* check if the table is clustered on this index */
+	if (indexForm->indisclustered)
+	{
+		char	   *qualifiedRelationName =
+			generate_qualified_relation_name(tableRelationId);
+		char	   *indexName = get_rel_name(indexRelationId);	/* needs to be quoted */
+
+		initStringInfo(&buffer);
+		appendStringInfo(&buffer, "ALTER TABLE %s CLUSTER ON %s",
+						 qualifiedRelationName, quote_identifier(indexName));
+	}
+
+	ReleaseSysCache(indexTuple);
+
+	return (buffer.data);
+}
+
+/*
+ * pg_get_table_grants returns a list of sql statements which recreate the
+ * permissions for a specific table.
+ *
+ * This function is modeled after aclexplode(), don't change too heavily.
+ */
+List *
+pg_get_table_grants(Oid relationId)
+{
+	StringInfoData buffer;
+	List	   *defs = NIL;
+	bool		isNull = false;
+
+	Relation	relation = relation_open(relationId, AccessShareLock);
+	char	   *relationName = generate_relation_name(relationId, NIL);
+	HeapTuple	classTuple;
+	Datum		aclDatum;
+
+	initStringInfo(&buffer);
+
+	/* lookup all table level grants */
+	classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relationId));
+	if (!HeapTupleIsValid(classTuple))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_TABLE),
+				 errmsg("relation with OID %u does not exist",
+						relationId)));
+	}
+
+	aclDatum = SysCacheGetAttr(RELOID, classTuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	ReleaseSysCache(classTuple);
+
+	if (!isNull)
+	{
+		Acl		   *acl = DatumGetAclP(aclDatum);
+		AclItem    *aidat = ACL_DAT(acl);
+
+		int			offtype = -1;
+		int			i = 0;
+
+		/*
+		 * First revoke all default permissions, so we can start adding the
+		 * exact permissions from the master. Note that we only do so if there
+		 * are any actual grants; an empty grant set signals default
+		 * permissions.
+		 *
+		 * Note: This doesn't work correctly if default permissions have been
+		 * changed with ALTER DEFAULT PRIVILEGES - but that's hard to fix
+		 * properly currently.
+		 */
+		appendStringInfo(&buffer, "REVOKE ALL ON %s FROM PUBLIC",
+						 relationName);
+		defs = lappend(defs, pstrdup(buffer.data));
+		resetStringInfo(&buffer);
+
+		/* iterate through the acl datastructure, emit GRANTs */
+		while (i < ACL_NUM(acl))
+		{
+			AclItem    *aidata = NULL;
+			AclMode		priv_bit = 0;
+
+			offtype++;
+
+			if (offtype == N_ACL_RIGHTS)
+			{
+				offtype = 0;
+				i++;
+				if (i >= ACL_NUM(acl))	/* done */
+				{
+					break;
+				}
+			}
+
+			aidata = &aidat[i];
+			priv_bit = 1 << offtype;
+
+			if (ACLITEM_GET_PRIVS(*aidata) & priv_bit)
+			{
+				const char *roleName = NULL;
+				const char *withGrant = "";
+
+				if (aidata->ai_grantee != 0)
+				{
+
+					HeapTuple	htup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(aidata->ai_grantee));
+
+					if (HeapTupleIsValid(htup))
+					{
+						Form_pg_authid authForm = ((Form_pg_authid) GETSTRUCT(htup));
+
+						roleName = quote_identifier(NameStr(authForm->rolname));
+
+						ReleaseSysCache(htup);
+					}
+					else
+					{
+						elog(ERROR, "cache lookup failed for role %u", aidata->ai_grantee);
+					}
+				}
+				else
+				{
+					roleName = "PUBLIC";
+				}
+
+				if ((ACLITEM_GET_GOPTIONS(*aidata) & priv_bit) != 0)
+				{
+					withGrant = " WITH GRANT OPTION";
+				}
+
+				appendStringInfo(&buffer, "GRANT %s ON %s TO %s%s",
+								 convert_aclright_to_string(priv_bit),
+								 relationName,
+								 roleName,
+								 withGrant);
+
+				defs = lappend(defs, pstrdup(buffer.data));
+
+				resetStringInfo(&buffer);
+			}
+		}
+	}
+
+	resetStringInfo(&buffer);
+
+	relation_close(relation, NoLock);
+	return defs;
+}
+
+/*
+ * pg_get_replica_identity_command function returns the required ALTER .. TABLE
+ * command to define the replica identity.
+ */
+char *
+pg_get_replica_identity_command(Oid tableRelationId)
+{
+	StringInfo	buf = makeStringInfo();
+
+	Relation	relation = table_open(tableRelationId, AccessShareLock);
+
+	char		replicaIdentity = relation->rd_rel->relreplident;
+
+	char	   *relationName = generate_qualified_relation_name(tableRelationId);
+
+	if (replicaIdentity == REPLICA_IDENTITY_INDEX)
+	{
+		Oid			indexId = RelationGetReplicaIndex(relation);
+
+		if (OidIsValid(indexId))
+		{
+			appendStringInfo(buf, "ALTER TABLE %s REPLICA IDENTITY USING INDEX %s ",
+							 relationName,
+							 quote_identifier(get_rel_name(indexId)));
+		}
+	}
+	else if (replicaIdentity == REPLICA_IDENTITY_NOTHING)
+	{
+		appendStringInfo(buf, "ALTER TABLE %s REPLICA IDENTITY NOTHING",
+						 relationName);
+	}
+	else if (replicaIdentity == REPLICA_IDENTITY_FULL)
+	{
+		appendStringInfo(buf, "ALTER TABLE %s REPLICA IDENTITY FULL",
+						 relationName);
+	}
+
+	table_close(relation, AccessShareLock);
+
+	return (buf->len > 0) ? buf->data : NULL;
+}
+
+/*
+ * pg_get_row_level_security_commands function returns the required ALTER .. TABLE
+ * commands to define the row level security settings for a relation.
+ */
+List *
+pg_get_row_level_security_commands(Oid relationId)
+{
+	StringInfoData buffer;
+	List	   *commands = NIL;
+	Relation	relation = table_open(relationId, AccessShareLock);
+
+	initStringInfo(&buffer);
+
+	if (relation->rd_rel->relrowsecurity)
+	{
+		char	   *relationName = generate_qualified_relation_name(relationId);
+
+		appendStringInfo(&buffer, "ALTER TABLE %s ENABLE ROW LEVEL SECURITY",
+						 relationName);
+		commands = lappend(commands, pstrdup(buffer.data));
+		resetStringInfo(&buffer);
+	}
+
+	if (relation->rd_rel->relforcerowsecurity)
+	{
+		char	   *relationName = generate_qualified_relation_name(relationId);
+
+		appendStringInfo(&buffer, "ALTER TABLE %s FORCE ROW LEVEL SECURITY",
+						 relationName);
+		commands = lappend(commands, pstrdup(buffer.data));
+		resetStringInfo(&buffer);
+	}
+
+	table_close(relation, AccessShareLock);
+
+	return commands;
+}
+
+/*
+ * contain_nextval_expression_walker walks over expression tree and returns
+ * true if it contains call to 'nextval' function or it has an identity column.
+ */
+bool
+contain_nextval_expression_walker(Node *node, void *context)
+{
+	if (node == NULL)
+	{
+		return false;
+	}
+
+	/* check if the node contains an identity column */
+	if (IsA(node, NextValueExpr))
+	{
+		return true;
+	}
+
+	/* check if the node contains call to 'nextval' */
+	if (IsA(node, FuncExpr))
+	{
+		FuncExpr   *funcExpr = (FuncExpr *) node;
+
+		if (funcExpr->funcid == F_NEXTVAL)
+		{
+			return true;
+		}
+	}
+	return expression_tree_walker(node, contain_nextval_expression_walker, context);
+}
+
+
+/*
+ * AppendOptionListToString converts the option list to its textual format, and
+ * appends this text to the given string buffer.
+ */
+void
+AppendOptionListToString(StringInfo stringBuffer, List *optionList)
+{
+	if (optionList != NIL)
+	{
+		ListCell   *optionCell = NULL;
+		bool		firstOptionPrinted = false;
+
+		appendStringInfo(stringBuffer, " OPTIONS (");
+
+		foreach(optionCell, optionList)
+		{
+			DefElem    *option = (DefElem *) lfirst(optionCell);
+			char	   *optionName = option->defname;
+			char	   *optionValue = defGetString(option);
+
+			if (firstOptionPrinted)
+			{
+				appendStringInfo(stringBuffer, ", ");
+			}
+			firstOptionPrinted = true;
+
+			appendStringInfo(stringBuffer, "%s ", quote_identifier(optionName));
+			appendStringInfo(stringBuffer, "%s", quote_literal_cstr(optionValue));
+		}
+
+		appendStringInfo(stringBuffer, ")");
+	}
+}
+
+/*
+ * AppendStorageParametersToString converts the storage parameter list to its
+ * textual format, and appends this text to the given string buffer.
+ */
+void
+AppendStorageParametersToString(StringInfo stringBuffer, List *optionList)
+{
+	ListCell   *optionCell = NULL;
+	bool		firstOptionPrinted = false;
+
+	foreach(optionCell, optionList)
+	{
+		DefElem    *option = (DefElem *) lfirst(optionCell);
+		char	   *optionName = option->defname;
+		char	   *optionValue = defGetString(option);
+
+		if (firstOptionPrinted)
+		{
+			appendStringInfo(stringBuffer, ", ");
+		}
+		firstOptionPrinted = true;
+
+		appendStringInfo(stringBuffer, "%s = %s ",
+						 quote_identifier(optionName),
+						 quote_literal_cstr(optionValue));
+	}
+}
+
+/*
+ * GenereatePartitioningInformation returns the partitioning type and partition column
+ * for the given parent table in the form of "PARTITION TYPE (partitioning column(s)/expression(s))".
+ */
+char *
+GeneratePartitioningInformation(Oid parentTableId)
+{
+	char	   *partitionBoundCString = "";
+	Datum		partitionBoundDatum;
+
+	if (!PartitionedTable(parentTableId))
+	{
+		char	   *relationName = get_rel_name(parentTableId);
+
+		ereport(ERROR, (errmsg("\"%s\" is not a parent table", relationName)));
+	}
+
+	partitionBoundDatum = DirectFunctionCall1(pg_get_partkeydef,
+											  ObjectIdGetDatum(parentTableId));
+
+	partitionBoundCString = TextDatumGetCString(partitionBoundDatum);
+
+	return partitionBoundCString;
+}
+
+/*
+ * Returns true if the given relation is a partitioned table.
+ */
+bool
+PartitionedTable(Oid relationId)
+{
+	Relation	rel = try_relation_open(relationId, AccessShareLock);
+	bool		partitionedTable = false;
+
+	/* don't error out for tables that are dropped */
+	if (rel == NULL)
+	{
+		return false;
+	}
+
+	if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+	{
+		partitionedTable = true;
+	}
+
+	/* keep the lock */
+	table_close(rel, NoLock);
+
+	return partitionedTable;
+}
+
+/*
+ * RoleSpecString resolves the role specification to its string form that is suitable for transport to a worker node.
+ * This function resolves the following identifiers from the current context so they are safe to transfer.
+ *
+ * CURRENT_USER - resolved to the user name of the current role being used
+ * SESSION_USER - resolved to the user name of the user that opened the session
+ * CURRENT_ROLE - same as CURRENT_USER, resolved to the user name of the current role being used
+ * Postgres treats CURRENT_ROLE is equivalent to CURRENT_USER, and we follow the same approach.
+ *
+ * withQuoteIdentifier is used, because if the results will be used in a query the quotes are needed but if not there
+ * should not be extra quotes.
+ */
+const char *
+RoleSpecString(RoleSpec *spec, bool withQuoteIdentifier)
+{
+	switch (spec->roletype)
+	{
+		case ROLESPEC_CSTRING:
+			{
+				return withQuoteIdentifier ?
+					quote_identifier(spec->rolename) :
+					spec->rolename;
+			}
+
+#if PG_VERSION_NUM >= PG_VERSION_14
+		case ROLESPEC_CURRENT_ROLE:
+#endif
+		case ROLESPEC_CURRENT_USER:
+			{
+				return withQuoteIdentifier ?
+					quote_identifier(GetUserNameFromId(GetUserId(), false)) :
+					GetUserNameFromId(GetUserId(), false);
+			}
+
+		case ROLESPEC_SESSION_USER:
+			{
+				return withQuoteIdentifier ?
+					quote_identifier(GetUserNameFromId(GetSessionUserId(), false)) :
+					GetUserNameFromId(GetSessionUserId(), false);
+			}
+
+		case ROLESPEC_PUBLIC:
+			{
+				return "PUBLIC";
+			}
+
+		default:
+			{
+				elog(ERROR, "unexpected role type %d", spec->roletype);
+			}
+	}
+}
+
+/*
+ * TableOwnerResetCommand generates a commands that can be executed
+ * to reset the table owner.
+ */
+char *
+TableOwnerResetCommand(Oid relationId)
+{
+	StringInfo	ownerResetCommand = makeStringInfo();
+	char	   *qualifiedRelationName = generate_qualified_relation_name(relationId);
+	char	   *tableOwnerName = TableOwner(relationId);
+
+	appendStringInfo(ownerResetCommand,
+					 "ALTER TABLE %s OWNER TO %s",
+					 qualifiedRelationName,
+					 quote_identifier(tableOwnerName));
+
+	return ownerResetCommand->data;
+}
+
+Oid
+TableOwnerOid(Oid relationId)
+{
+	Oid			userId;
+	HeapTuple	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relationId));
+
+	if (!HeapTupleIsValid(tuple))
+	{
+		ereport(ERROR, (errcode(ERRCODE_UNDEFINED_TABLE),
+						errmsg("relation with OID %u does not exist", relationId)));
+	}
+
+	userId = ((Form_pg_class) GETSTRUCT(tuple))->relowner;
+
+	ReleaseSysCache(tuple);
+	return userId;
+}
+
+/*
+ * Return a table's owner as a string.
+ */
+char *
+TableOwner(Oid relationId)
+{
+	return GetUserNameFromId(TableOwnerOid(relationId), false);
+}
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index f8e1238fa2c..c384a455950 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -275,5 +275,6 @@ extern void removeExtObjInitPriv(Oid objoid, Oid classoid);
 extern bool object_ownercheck(Oid classid, Oid objectid, Oid roleid);
 extern bool has_createrole_privilege(Oid roleid);
 extern bool has_bypassrls_privilege(Oid roleid);
+extern const char *convert_aclright_to_string(int aclright);
 
 #endif							/* ACL_H */
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index b006d9d475e..ea81a68545d 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -16,6 +16,7 @@
 #include "nodes/nodes.h"
 #include "nodes/parsenodes.h"
 #include "nodes/pg_list.h"
+#include "catalog/pg_sequence.h"
 
 struct Plan;					/* avoid including plannodes.h here */
 struct PlannedStmt;
@@ -24,6 +25,27 @@ struct PlannedStmt;
 #define RULE_INDEXDEF_PRETTY		0x01
 #define RULE_INDEXDEF_KEYS_ONLY		0x02	/* ignore included attributes */
 
+/*
+ * IncludeSequenceDefaults decides on inclusion of DEFAULT clauses for columns
+ * getting their default values from a sequence when creating the definition
+ * of a table.
+ */
+typedef enum IncludeSequenceDefaults
+{
+	NO_SEQUENCE_DEFAULTS = 0,	/* don't include sequence defaults */
+	INCLUDE_SEQUENCE_DEFAULTS = 1,	/* include sequence defaults */
+} IncludeSequenceDefaults;
+
+/*
+ * IncludeIdentities decides on how we include identity information
+ * when creating the definition of a table.
+ */
+typedef enum IncludeIdentities
+{
+	NO_IDENTITY = 0,			/* don't include identities */
+	INCLUDE_IDENTITY = 1		/* include identities as-is */
+} IncludeIdentities;
+
 extern char *pg_get_indexdef_string(Oid indexrelid);
 extern char *pg_get_indexdef_columns(Oid indexrelid, bool pretty);
 extern char *pg_get_indexdef_columns_extended(Oid indexrelid,
@@ -43,10 +65,39 @@ extern List *set_deparse_context_plan(List *dpcontext,
 									  struct Plan *plan, List *ancestors);
 extern List *select_rtable_names_for_explain(List *rtable,
 											 Bitmapset *rels_used);
+extern char *generate_qualified_relation_name(Oid collid);
 extern char *generate_collation_name(Oid collid);
 extern char *generate_opclass_name(Oid opclass);
 extern char *get_range_partbound_string(List *bound_datums);
 
 extern char *pg_get_statisticsobjdef_string(Oid statextid);
 
+/* Function declarations for version independent Citus ruleutils wrapper functions */
+extern char *pg_get_extensiondef_string(Oid tableRelationId);
+extern char *get_extension_version(Oid extensionId);
+extern char *pg_get_serverdef_string(Oid tableRelationId);
+extern char *pg_get_sequencedef_string(Oid sequenceRelid);
+extern Form_pg_sequence pg_get_sequencedef(Oid sequenceRelationId);
+extern char *pg_get_tableschemadef_string(Oid tableRelationId,
+										  IncludeSequenceDefaults includeSequenceDefaults,
+										  IncludeIdentities includeIdentityDefaults,
+										  char *accessMethod);
+extern char *pg_get_tablecolumnoptionsdef_string(Oid tableRelationId);
+extern char *pg_get_indexclusterdef_string(Oid indexRelationId);
+extern List *pg_get_table_grants(Oid relationId);
+extern char *pg_get_replica_identity_command(Oid tableRelationId);
+extern List *pg_get_row_level_security_commands(Oid relationId);
+extern bool contain_nextval_expression_walker(Node *node, void *context);
+extern void AppendOptionListToString(StringInfo stringData, List *options);
+extern void AppendStorageParametersToString(StringInfo stringBuffer,
+											List *optionList);
+
+extern bool RegularTable(Oid relationId);
+extern char *GeneratePartitioningInformation(Oid parentTableId);
+extern bool PartitionedTable(Oid relationId);
+extern const char *RoleSpecString(RoleSpec *spec, bool withQuoteIdentifier);
+extern char *TableOwnerResetCommand(Oid relationId);
+extern Oid	TableOwnerOid(Oid relationId);
+extern char *TableOwner(Oid relationId);
+
 #endif							/* RULEUTILS_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 260854747b4..241594e6333 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1141,6 +1141,8 @@ ImportForeignSchema_function
 ImportQual
 InProgressEnt
 IncludeWal
+IncludeSequenceDefaults
+IncludeIdentities
 InclusionOpaque
 IncrementVarSublevelsUp_context
 IncrementalSort

base-commit: 8cddea9a539cbbdd1b316255c9f404323243fd37
-- 
2.34.1

#44Kirk Wolak
wolakk@gmail.com
In reply to: Jelte Fennema (#43)
Re: Adding SHOW CREATE TABLE

On Mon, Jun 5, 2023 at 7:43 AM Jelte Fennema <postgres@jeltef.nl> wrote:

On Thu, 1 Jun 2023 at 18:57, Kirk Wolak <wolakk@gmail.com> wrote:

Can this get turned into a Patch? Were you offering this code up for

others (me?) to pull, and work into a patch?

[If I do the patch, I am not sure it gives you the value of reducing

what CITUS has to maintain. But it dawns on

me that you might be pushing a much bigger patch... But I would take

that, as I think there is other value in there]

Attached is a patch which adds the relevant functions from Citus to
Postgres (it compiles without warnings on my machine). I checked with
a few others on the Citus team at Microsoft and everyone thought that
upstreaming this was a good idea, because it's quite a bit of work to
update with every postgres release.

To set expectations though, I don't really have time to work on this
patch. So if you can take it over from here that would be great.

The patch only contains the C functions which generate SQL based on
some oids. The wrappers such as the master_get_table_ddl_events
function were too hard for me to pull out of Citus code, because they
integrated a lot with other pieces. But the bulk of the logic is in
the functions in this patch. Essentially all that
master_get_table_ddl_events does is call the functions in this patch
in the right order.

The ONLY thing I did not see was "CREATE TEMPORARY " syntax? If you did

this on a TEMP table,

does it generate normal table syntax or TEMPORARY TABLE syntax???

Yeah, the Citus code only handles things that Citus supports in
distributed tables. Which is quite a lot, but indeed not everything
yet. Temporary and inherited tables are not supported in this code
afaik. Possibly more. See the commented out
EnsureRelationKindSupported for what should be supported (normal
tables and partitioned tables afaik).

Jelte,
Thank you for this.
Let me see what I can do with this, it seems like a solid starting point.
At this point, based on previous feedback, finding a way to make
get_tabledef() etc. to work as functions is my goal.
I will see how inherited tables and temporary tables will be dealt with.

Hopefully, this transfer works to please anyone concerned with
integrating this code into our project from the Citus code.

Kirk...

#45Kirk Wolak
wolakk@gmail.com
In reply to: Kirk Wolak (#44)
Re: Adding SHOW CREATE TABLE

On Wed, Jun 21, 2023 at 8:52 PM Kirk Wolak <wolakk@gmail.com> wrote:

On Mon, Jun 5, 2023 at 7:43 AM Jelte Fennema <postgres@jeltef.nl> wrote:

On Thu, 1 Jun 2023 at 18:57, Kirk Wolak <wolakk@gmail.com> wrote:

Can this get turned into a Patch? Were you offering this code up for

others (me?) to pull, and work into a patch?

[If I do the patch, I am not sure it gives you the value of reducing

what CITUS has to maintain. But it dawns on

me that you might be pushing a much bigger patch... But I would take

that, as I think there is other value in there]

Yeah, the Citus code only handles things that Citus supports in

distributed tables. Which is quite a lot, but indeed not everything
yet. Temporary and inherited tables are not supported in this code
afaik. Possibly more. See the commented out
EnsureRelationKindSupported for what should be supported (normal
tables and partitioned tables afaik).

Okay, apologies for the long delay on this. I have the code Jelte
submitted working. And I have (almost) figured out how to add the function
so it shows up in the pg_catalog... (I edited files I should not have, I
need to know the proper process... Anyone...)

Not sure if it is customary to attach the code when asking about stuff.
For the most part, it was what Jelte Gave us with a pg_get_tabledef()
wrapper to call...

Here is the output it produces for *select
pg_get_tabledef('pg_class'::regclass); * (Feedback Welcome)

CREATE TABLE pg_class (oid oid NOT NULL, relname name NOT NULL COLLATE "C",
relnamespace oid NOT NULL, reltype oid NOT NULL, reloftype oid NOT NULL,
relowner oid NOT NULL, relam oid NOT NULL, relfilenode oid NOT NULL,
reltablespace oid NOT NULL, relpages integer NOT NULL, reltuples real NOT
NULL, relallvisible integer NOT NULL, reltoastrelid oid NOT NULL,
relhasindex boolean NOT NULL, relisshared boolean NOT NULL, relpersistence
"char" NOT NULL, relkind "char" NOT NULL, relnatts smallint NOT NULL,
relchecks smallint NOT NULL, relhasrules boolean NOT NULL, relhastriggers
boolean NOT NULL, relhassubclass boolean NOT NULL, relrowsecurity boolean
NOT NULL, relforcerowsecurity boolean NOT NULL, relispopulated boolean NOT
NULL, relreplident "char" NOT NULL, relispartition boolean NOT NULL,
relrewrite oid NOT NULL, relfrozenxid xid NOT NULL, relminmxid xid NOT
NULL, relacl aclitem[], reloptions text[] COLLATE "C", relpartbound
pg_node_tree COLLATE "C") USING heap

==
My Comments/Questions:
1) I would prefer Legible output, like below
2) I would prefer to leave off COLLATE "C" IFF that is the DB Default
3) The USING heap... I want to pull UNLESS the value is NOT the default
(That's a theme in my comments)
4) I *THINK* including the schema would be nice?
5) This version will work with a TEMP table, but NOT EMIT "TEMPORARY"...
Thoughts? Is emitting [pg_temp.] good enough?
6) This version enumerates sequence values (Drop always, or Drop if they
are the default values?)
7) Should I enable the pg_get_seqdef() code
8) It does NOT handle Inheritance (Yet... Is this important? Is it okay to
just give the table structure for this table?)
9) I have not tested against Partitions, etc... I SIMPLY want initial
feedback on Formatting

-- Legible:
CREATE TABLE pg_class (oid oid NOT NULL,
relname name NOT NULL COLLATE "C",
relnamespace oid NOT NULL,
reltype oid NOT NULL,
...
reloptions text[] COLLATE "C",
relpartbound pg_node_tree COLLATE "C"
)

-- Too verbose with "*DEFAULT*" Sequence Values:
CREATE TABLE t1 (id bigint GENERATED BY DEFAULT AS IDENTITY *(INCREMENT BY
1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1 NO CYCLE)*
NOT NULL,
f1 text
) WITH (autovacuum_vacuum_cost_delay='0', fillfactor='80',
autovacuum_vacuum_insert_threshold='-1',
autovacuum_analyze_threshold='500000000',
autovacuum_vacuum_threshold='500000000',
autovacuum_vacuum_scale_factor='1.5')

Thanks,

Kirk...
PS: After I get feedback on Formatting the output, etc. I will gladly
generate a new .patch file and send it along. Otherwise Jelte gets 100% of
the credit, and I don't want to look like I am changing that.

#46Kirk Wolak
wolakk@gmail.com
In reply to: Kirk Wolak (#45)
Re: Adding SHOW CREATE TABLE

On Fri, Jun 30, 2023 at 1:56 PM Kirk Wolak <wolakk@gmail.com> wrote:

On Wed, Jun 21, 2023 at 8:52 PM Kirk Wolak <wolakk@gmail.com> wrote:

On Mon, Jun 5, 2023 at 7:43 AM Jelte Fennema <postgres@jeltef.nl> wrote:

On Thu, 1 Jun 2023 at 18:57, Kirk Wolak <wolakk@gmail.com> wrote:

Definitely have the questions from the previous email, but I CERTAINLY
appreciate this output.
(Don't like the +, but pg_get_viewdef() creates the view the same way)...
Will psql doing \st pg_class be able to just call/output this so that the
output is nice and clean?

At this point... I will keep pressing forward, cleaning things up. And
then send a patch for others to play with....
(Probably bad timing with wrapping up V16)

*select pg_get_tabledef('pg_class'::regclass);*
pg_get_tabledef
----------------------------------------
CREATE TABLE pg_class (oid oid NOT NULL,+
relname name NOT NULL COLLATE "C", +
relnamespace oid NOT NULL, +
reltype oid NOT NULL, +
reloftype oid NOT NULL, +
relowner oid NOT NULL, +
relam oid NOT NULL, +
relfilenode oid NOT NULL, +
reltablespace oid NOT NULL, +
relpages integer NOT NULL, +
reltuples real NOT NULL, +
relallvisible integer NOT NULL, +
reltoastrelid oid NOT NULL, +
relhasindex boolean NOT NULL, +
relisshared boolean NOT NULL, +
relpersistence "char" NOT NULL, +
relkind "char" NOT NULL, +
relnatts smallint NOT NULL, +
relchecks smallint NOT NULL, +
relhasrules boolean NOT NULL, +
relhastriggers boolean NOT NULL, +
relhassubclass boolean NOT NULL, +
relrowsecurity boolean NOT NULL, +
relforcerowsecurity boolean NOT NULL, +
relispopulated boolean NOT NULL, +
relreplident "char" NOT NULL, +
relispartition boolean NOT NULL, +
relrewrite oid NOT NULL, +
relfrozenxid xid NOT NULL, +
relminmxid xid NOT NULL, +
relacl aclitem[], +
reloptions text[] COLLATE "C", +
relpartbound pg_node_tree COLLATE "C" +
) USING heap