diff --git a/Makefile b/Makefile index 104ae46..584efa3 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ MODULES = wal2json REGRESS = cmdline insert1 update1 update2 update3 update4 delete1 delete2 \ delete3 delete4 savepoint specialvalue toast bytea message typmod \ filtertable selecttable include_timestamp include_lsn include_xids \ - include_domain_data_type truncate actions position + include_domain_data_type truncate actions position default PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/README.md b/README.md index 62f1ef1..111f5bc 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ Parameters * `include-domain-data-type`: replace domain name with the underlying data type. Default is _false_. * `include-column-positions`: add column position (_pg_attribute.attnum_). Default is _false_. * `include-not-null`: add _not null_ information as _columnoptionals_. Default is _false_. +* `include-default`: add default expression. Default is _false_. * `pretty-print`: add spaces and indentation to JSON structures. Default is _false_. * `write-in-chunks`: write after every change instead of every changeset. Default is _false_. * `include-lsn`: add _nextlsn_ to each changeset. Default is _false_. diff --git a/expected/default.out b/expected/default.out new file mode 100644 index 0000000..69c85f5 --- /dev/null +++ b/expected/default.out @@ -0,0 +1,66 @@ +\set VERBOSITY terse +-- predictability +SET synchronous_commit = on; +CREATE TABLE w2j_default (a serial, b integer DEFAULT 6, c text DEFAULT 'wal2json', d timestamp DEFAULT '2020-07-12 11:55:30', e integer DEFAULT NULL, f integer, PRIMARY KEY(a)); +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'wal2json'); + ?column? +---------- + init +(1 row) + +INSERT INTO w2j_default (b, c ,d, e, f) VALUES(2, 'test', '2020-03-01 08:09:00', 80, 10); +INSERT INTO w2j_default DEFAULT VALUES; +UPDATE w2j_default SET b = 3 WHERE a = 1; +-- without include-default parameter +SELECT data FROM pg_logical_slot_peek_changes('regression_slot', NULL, NULL, 'format-version', '1'); + data +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + {"change":[{"kind":"insert","schema":"public","table":"w2j_default","columnnames":["a","b","c","d","e","f"],"columntypes":["integer","integer","text","timestamp without time zone","integer","integer"],"columnvalues":[1,2,"test","Sun Mar 01 08:09:00 2020",80,10]}]} + {"change":[{"kind":"insert","schema":"public","table":"w2j_default","columnnames":["a","b","c","d","e","f"],"columntypes":["integer","integer","text","timestamp without time zone","integer","integer"],"columnvalues":[2,6,"wal2json","Sun Jul 12 11:55:30 2020",null,null]}]} + {"change":[{"kind":"update","schema":"public","table":"w2j_default","columnnames":["a","b","c","d","e","f"],"columntypes":["integer","integer","text","timestamp without time zone","integer","integer"],"columnvalues":[1,3,"test","Sun Mar 01 08:09:00 2020",80,10],"oldkeys":{"keynames":["a"],"keytypes":["integer"],"keyvalues":[1]}}]} +(3 rows) + +SELECT data FROM pg_logical_slot_peek_changes('regression_slot', NULL, NULL, 'format-version', '2'); + data +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + {"action":"B"} + {"action":"I","schema":"public","table":"w2j_default","columns":[{"name":"a","type":"integer","value":1},{"name":"b","type":"integer","value":2},{"name":"c","type":"text","value":"test"},{"name":"d","type":"timestamp without time zone","value":"Sun Mar 01 08:09:00 2020"},{"name":"e","type":"integer","value":80},{"name":"f","type":"integer","value":10}]} + {"action":"C"} + {"action":"B"} + {"action":"I","schema":"public","table":"w2j_default","columns":[{"name":"a","type":"integer","value":2},{"name":"b","type":"integer","value":6},{"name":"c","type":"text","value":"wal2json"},{"name":"d","type":"timestamp without time zone","value":"Sun Jul 12 11:55:30 2020"},{"name":"e","type":"integer","value":null},{"name":"f","type":"integer","value":null}]} + {"action":"C"} + {"action":"B"} + {"action":"U","schema":"public","table":"w2j_default","columns":[{"name":"a","type":"integer","value":1},{"name":"b","type":"integer","value":3},{"name":"c","type":"text","value":"test"},{"name":"d","type":"timestamp without time zone","value":"Sun Mar 01 08:09:00 2020"},{"name":"e","type":"integer","value":80},{"name":"f","type":"integer","value":10}],"identity":[{"name":"a","type":"integer","value":1}]} + {"action":"C"} +(9 rows) + +-- with include-default parameter +SELECT data FROM pg_logical_slot_peek_changes('regression_slot', NULL, NULL, 'format-version', '1', 'include-default', '1'); + data +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + {"change":[{"kind":"insert","schema":"public","table":"w2j_default","columnnames":["a","b","c","d","e","f"],"columntypes":["integer","integer","text","timestamp without time zone","integer","integer"],"columndefaults":["nextval('w2j_default_a_seq'::regclass)","6","'wal2json'::text","'Sun Jul 12 11:55:30 2020'::timestamp without time zone",null,null],"columnvalues":[1,2,"test","Sun Mar 01 08:09:00 2020",80,10]}]} + {"change":[{"kind":"insert","schema":"public","table":"w2j_default","columnnames":["a","b","c","d","e","f"],"columntypes":["integer","integer","text","timestamp without time zone","integer","integer"],"columndefaults":["nextval('w2j_default_a_seq'::regclass)","6","'wal2json'::text","'Sun Jul 12 11:55:30 2020'::timestamp without time zone",null,null],"columnvalues":[2,6,"wal2json","Sun Jul 12 11:55:30 2020",null,null]}]} + {"change":[{"kind":"update","schema":"public","table":"w2j_default","columnnames":["a","b","c","d","e","f"],"columntypes":["integer","integer","text","timestamp without time zone","integer","integer"],"columndefaults":["nextval('w2j_default_a_seq'::regclass)","6","'wal2json'::text","'Sun Jul 12 11:55:30 2020'::timestamp without time zone",null,null],"columnvalues":[1,3,"test","Sun Mar 01 08:09:00 2020",80,10],"oldkeys":{"keynames":["a"],"keytypes":["integer"],"keyvalues":[1]}}]} +(3 rows) + +SELECT data FROM pg_logical_slot_peek_changes('regression_slot', NULL, NULL, 'format-version', '2', 'include-default', '1'); + data +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + {"action":"B"} + {"action":"I","schema":"public","table":"w2j_default","columns":[{"name":"a","type":"integer","value":1,"default":"nextval('w2j_default_a_seq'::regclass)"},{"name":"b","type":"integer","value":2,"default":"6"},{"name":"c","type":"text","value":"test","default":"'wal2json'::text"},{"name":"d","type":"timestamp without time zone","value":"Sun Mar 01 08:09:00 2020","default":"'Sun Jul 12 11:55:30 2020'::timestamp without time zone"},{"name":"e","type":"integer","value":80,"default":null},{"name":"f","type":"integer","value":10,"default":null}]} + {"action":"C"} + {"action":"B"} + {"action":"I","schema":"public","table":"w2j_default","columns":[{"name":"a","type":"integer","value":2,"default":"nextval('w2j_default_a_seq'::regclass)"},{"name":"b","type":"integer","value":6,"default":"6"},{"name":"c","type":"text","value":"wal2json","default":"'wal2json'::text"},{"name":"d","type":"timestamp without time zone","value":"Sun Jul 12 11:55:30 2020","default":"'Sun Jul 12 11:55:30 2020'::timestamp without time zone"},{"name":"e","type":"integer","value":null,"default":null},{"name":"f","type":"integer","value":null,"default":null}]} + {"action":"C"} + {"action":"B"} + {"action":"U","schema":"public","table":"w2j_default","columns":[{"name":"a","type":"integer","value":1,"default":"nextval('w2j_default_a_seq'::regclass)"},{"name":"b","type":"integer","value":3,"default":"6"},{"name":"c","type":"text","value":"test","default":"'wal2json'::text"},{"name":"d","type":"timestamp without time zone","value":"Sun Mar 01 08:09:00 2020","default":"'Sun Jul 12 11:55:30 2020'::timestamp without time zone"},{"name":"e","type":"integer","value":80,"default":null},{"name":"f","type":"integer","value":10,"default":null}],"identity":[{"name":"a","type":"integer","value":1}]} + {"action":"C"} +(9 rows) + +SELECT 'stop' FROM pg_drop_replication_slot('regression_slot'); + ?column? +---------- + stop +(1 row) + +DROP TABLE w2j_default; diff --git a/sql/default.sql b/sql/default.sql new file mode 100644 index 0000000..8d3d107 --- /dev/null +++ b/sql/default.sql @@ -0,0 +1,24 @@ +\set VERBOSITY terse + +-- predictability +SET synchronous_commit = on; + +CREATE TABLE w2j_default (a serial, b integer DEFAULT 6, c text DEFAULT 'wal2json', d timestamp DEFAULT '2020-07-12 11:55:30', e integer DEFAULT NULL, f integer, PRIMARY KEY(a)); + +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'wal2json'); + +INSERT INTO w2j_default (b, c ,d, e, f) VALUES(2, 'test', '2020-03-01 08:09:00', 80, 10); +INSERT INTO w2j_default DEFAULT VALUES; +UPDATE w2j_default SET b = 3 WHERE a = 1; + +-- without include-default parameter +SELECT data FROM pg_logical_slot_peek_changes('regression_slot', NULL, NULL, 'format-version', '1'); +SELECT data FROM pg_logical_slot_peek_changes('regression_slot', NULL, NULL, 'format-version', '2'); + +-- with include-default parameter +SELECT data FROM pg_logical_slot_peek_changes('regression_slot', NULL, NULL, 'format-version', '1', 'include-default', '1'); +SELECT data FROM pg_logical_slot_peek_changes('regression_slot', NULL, NULL, 'format-version', '2', 'include-default', '1'); + +SELECT 'stop' FROM pg_drop_replication_slot('regression_slot'); + +DROP TABLE w2j_default; diff --git a/wal2json.c b/wal2json.c index c8f42a0..b4d3950 100644 --- a/wal2json.c +++ b/wal2json.c @@ -12,6 +12,10 @@ */ #include "postgres.h" +#include "access/genam.h" +#include "access/heapam.h" +#include "catalog/indexing.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_type.h" #include "replication/logical.h" @@ -20,6 +24,7 @@ #endif #include "utils/builtins.h" +#include "utils/fmgroids.h" #include "utils/guc.h" #include "utils/json.h" #include "utils/lsyscache.h" @@ -58,6 +63,7 @@ typedef struct bool include_domain_data_type; /* include underlying data type of the domain */ bool include_column_positions; /* include column numbers */ bool include_not_null; /* include not-null constraints */ + bool include_default; /* include default expressions */ bool pretty_print; /* pretty-print JSON? */ bool write_in_chunks; /* write in chunks? */ @@ -126,6 +132,8 @@ static void pg_decode_truncate(LogicalDecodingContext *ctx, ReorderBufferChange *change); #endif +static void columns_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tuple, bool hasreplident, Oid reloid); +static void tuple_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tuple, TupleDesc indexdesc, bool replident, bool hasreplident, Oid reloid); static bool parse_table_identifier(List *qualified_tables, char separator, List **select_tables); static bool string_to_SelectTable(char *rawstring, char separator, List **select_tables); static bool split_string_to_list(char *rawstring, char separator, List **sl); @@ -247,6 +255,7 @@ pg_decode_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt, bool is data->write_in_chunks = false; data->include_lsn = false; data->include_not_null = false; + data->include_default = false; data->filter_origins = NIL; data->filter_tables = NIL; data->filter_msg_prefixes = NIL; @@ -435,6 +444,19 @@ pg_decode_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt, bool is errmsg("could not parse value \"%s\" for parameter \"%s\"", strVal(elem->arg), elem->defname))); } + else if (strcmp(elem->defname, "include-default") == 0) + { + if (elem->arg == NULL) + { + elog(DEBUG1, "include-default argument is null"); + data->include_default = true; + } + else if (!parse_bool(strVal(elem->arg), &data->include_default)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse value \"%s\" for parameter \"%s\"", + strVal(elem->arg), elem->defname))); + } else if (strcmp(elem->defname, "pretty-print") == 0) { if (elem->arg == NULL) @@ -898,7 +920,6 @@ pg_decode_commit_txn_v2(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, OutputPluginWrite(ctx, true); } - /* * Accumulate tuple information and stores it at the end * @@ -906,7 +927,7 @@ pg_decode_commit_txn_v2(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, * hasreplident: does this tuple has an associated replica identity? */ static void -tuple_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tuple, TupleDesc indexdesc, bool replident, bool hasreplident) +tuple_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tuple, TupleDesc indexdesc, bool replident, bool hasreplident, Oid reloid) { JsonDecodingData *data; int natt; @@ -916,9 +937,12 @@ tuple_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tu StringInfoData coltypeoids; StringInfoData colpositions; StringInfoData colnotnulls; + StringInfoData coldefaults; StringInfoData colvalues; char comma[3] = ""; + Relation defrel = NULL; + data = ctx->output_plugin_private; initStringInfo(&colnames); @@ -929,6 +953,8 @@ tuple_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tu initStringInfo(&colpositions); if (data->include_not_null) initStringInfo(&colnotnulls); + if (data->include_default) + initStringInfo(&coldefaults); initStringInfo(&colvalues); /* @@ -955,9 +981,20 @@ tuple_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tu appendStringInfo(&colpositions, "%s%s%s\"columnpositions\":%s[", data->ht, data->ht, data->ht, data->sp); if (data->include_not_null) appendStringInfo(&colnotnulls, "%s%s%s\"columnoptionals\":%s[", data->ht, data->ht, data->ht, data->sp); + if (data->include_default) + appendStringInfo(&coldefaults, "%s%s%s\"columndefaults\":%s[", data->ht, data->ht, data->ht, data->sp); appendStringInfo(&colvalues, "%s%s%s\"columnvalues\":%s[", data->ht, data->ht, data->ht, data->sp); } + if (!replident && data->include_default) + { +#if PG_VERSION_NUM >= 120000 + defrel = table_open(AttrDefaultRelationId, AccessShareLock); +#else + defrel = heap_open(AttrDefaultRelationId, AccessShareLock); +#endif + } + /* Print column information (name, type, value) */ for (natt = 0; natt < tupdesc->natts; natt++) { @@ -1104,6 +1141,71 @@ tuple_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tu if (!replident && data->include_column_positions) appendStringInfo(&colpositions, "%s%d", comma, attr->attnum); + /* + * Print default for columns. + */ + if (!replident && data->include_default) + { +#if PG_VERSION_NUM >= 120000 + if (attr->atthasdef && attr->attgenerated == '\0') +#else + if (attr->atthasdef) +#endif + { + ScanKeyData scankeys[2]; + SysScanDesc scan; + HeapTuple def_tuple; + Datum def_value; + bool isnull; + char *result; + + ScanKeyInit(&scankeys[0], + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(reloid)); + ScanKeyInit(&scankeys[1], + Anum_pg_attrdef_adnum, + BTEqualStrategyNumber, F_INT2EQ, + Int16GetDatum(attr->attnum)); + + scan = systable_beginscan(defrel, AttrDefaultIndexId, true, + NULL, 2, scankeys); + + def_tuple = systable_getnext(scan); + if (HeapTupleIsValid(def_tuple)) + { + def_value = fastgetattr(def_tuple, Anum_pg_attrdef_adbin, defrel->rd_att, &isnull); + + if (!isnull) + { + result = TextDatumGetCString(DirectFunctionCall2(pg_get_expr, + def_value, + ObjectIdGetDatum(tuple->t_tableOid))); + + appendStringInfo(&coldefaults, "%s\"%s\"", comma, result); + pfree(result); + } + else + { + /* + * null means that default was not set. Is it possible? + * atthasdef shouldn't be set. + */ + appendStringInfo(&coldefaults, "%snull", comma); + } + } + + systable_endscan(scan); + } + else + { + /* + * no DEFAULT clause implicitly means that the default is NULL + */ + appendStringInfo(&coldefaults, "%snull", comma); + } + } + if (isnull) { appendStringInfo(&colvalues, "%snull", comma); @@ -1169,6 +1271,15 @@ tuple_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tu snprintf(comma, 3, ",%s", data->sp); } + if (!replident && data->include_default) + { +#if PG_VERSION_NUM >= 120000 + table_close(defrel, AccessShareLock); +#else + heap_close(defrel, AccessShareLock); +#endif + } + /* Column info ends */ if (replident) { @@ -1191,6 +1302,8 @@ tuple_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tu appendStringInfo(&colpositions, "],%s", data->nl); if (data->include_not_null) appendStringInfo(&colnotnulls, "],%s", data->nl); + if (data->include_default) + appendStringInfo(&coldefaults, "],%s", data->nl); if (hasreplident) appendStringInfo(&colvalues, "],%s", data->nl); else @@ -1207,6 +1320,8 @@ tuple_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tu appendStringInfoString(ctx->out, colpositions.data); if (data->include_not_null) appendStringInfoString(ctx->out, colnotnulls.data); + if (data->include_default) + appendStringInfoString(ctx->out, coldefaults.data); appendStringInfoString(ctx->out, colvalues.data); pfree(colnames.data); @@ -1217,14 +1332,16 @@ tuple_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tu pfree(colpositions.data); if (data->include_not_null) pfree(colnotnulls.data); + if (data->include_default) + pfree(coldefaults.data); pfree(colvalues.data); } /* Print columns information */ static void -columns_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tuple, bool hasreplident) +columns_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tuple, bool hasreplident, Oid reloid) { - tuple_to_stringinfo(ctx, tupdesc, tuple, NULL, false, hasreplident); + tuple_to_stringinfo(ctx, tupdesc, tuple, NULL, false, hasreplident, reloid); } /* Print replica identity information */ @@ -1232,7 +1349,7 @@ static void identity_to_stringinfo(LogicalDecodingContext *ctx, TupleDesc tupdesc, HeapTuple tuple, TupleDesc indexdesc) { /* Last parameter does not matter */ - tuple_to_stringinfo(ctx, tupdesc, tuple, indexdesc, true, false); + tuple_to_stringinfo(ctx, tupdesc, tuple, indexdesc, true, false, InvalidOid); } /* Callback for individual changed tuples */ @@ -1471,11 +1588,11 @@ pg_decode_change_v1(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, { case REORDER_BUFFER_CHANGE_INSERT: /* Print the new tuple */ - columns_to_stringinfo(ctx, tupdesc, &change->data.tp.newtuple->tuple, false); + columns_to_stringinfo(ctx, tupdesc, &change->data.tp.newtuple->tuple, false, change->data.tp.relnode.relNode); break; case REORDER_BUFFER_CHANGE_UPDATE: /* Print the new tuple */ - columns_to_stringinfo(ctx, tupdesc, &change->data.tp.newtuple->tuple, true); + columns_to_stringinfo(ctx, tupdesc, &change->data.tp.newtuple->tuple, true, change->data.tp.relnode.relNode); /* * The old tuple is available when: @@ -1627,6 +1744,7 @@ pg_decode_write_tuple(LogicalDecodingContext *ctx, Relation relation, HeapTuple { JsonDecodingData *data; TupleDesc tupdesc; + Relation defrel = NULL; Relation idxrel; TupleDesc idxdesc = NULL; int i; @@ -1668,6 +1786,16 @@ pg_decode_write_tuple(LogicalDecodingContext *ctx, Relation relation, HeapTuple elog(ERROR, "table does not have primary key or replica identity"); } + /* open pg_attrdef in preparation to get default values from columns */ + if (kind == PGOUTPUTJSON_CHANGE && data->include_default) + { +#if PG_VERSION_NUM >= 120000 + defrel = table_open(AttrDefaultRelationId, AccessShareLock); +#else + defrel = heap_open(AttrDefaultRelationId, AccessShareLock); +#endif + } + for (i = 0; i < tupdesc->natts; i++) { Form_pg_attribute attr; @@ -1777,9 +1905,85 @@ pg_decode_write_tuple(LogicalDecodingContext *ctx, Relation relation, HeapTuple appendStringInfo(ctx->out, "%d", attr->attnum); } + /* + * Print default for columns. + */ + if (kind == PGOUTPUTJSON_CHANGE && data->include_default) + { +#if PG_VERSION_NUM >= 120000 + if (attr->atthasdef && attr->attgenerated == '\0') +#else + if (attr->atthasdef) +#endif + { + ScanKeyData scankeys[2]; + SysScanDesc scan; + HeapTuple def_tuple; + Datum def_value; + bool isnull; + char *result; + + ScanKeyInit(&scankeys[0], + Anum_pg_attrdef_adrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relation->rd_id)); + ScanKeyInit(&scankeys[1], + Anum_pg_attrdef_adnum, + BTEqualStrategyNumber, F_INT2EQ, + Int16GetDatum(attr->attnum)); + + scan = systable_beginscan(defrel, AttrDefaultIndexId, true, + NULL, 2, scankeys); + + def_tuple = systable_getnext(scan); + if (HeapTupleIsValid(def_tuple)) + { + def_value = fastgetattr(def_tuple, Anum_pg_attrdef_adbin, defrel->rd_att, &isnull); + + if (!isnull) + { + result = TextDatumGetCString(DirectFunctionCall2(pg_get_expr, + def_value, + ObjectIdGetDatum(relation->rd_id))); + + appendStringInfoString(ctx->out, ",\"default\":"); + appendStringInfo(ctx->out, "\"%s\"", result); + pfree(result); + } + else + { + /* + * null means that default was not set. Is it possible? + * atthasdef shouldn't be set. + */ + appendStringInfoString(ctx->out, ",\"default\":null"); + } + } + + systable_endscan(scan); + } + else + { + /* + * no DEFAULT clause implicitly means that the default is NULL + */ + appendStringInfoString(ctx->out, ",\"default\":null"); + } + } + appendStringInfoChar(ctx->out, '}'); } + /* close pg_attrdef */ + if (kind == PGOUTPUTJSON_CHANGE && data->include_default) + { +#if PG_VERSION_NUM >= 120000 + table_close(defrel, AccessShareLock); +#else + heap_close(defrel, AccessShareLock); +#endif + } + pfree(values); pfree(nulls); }