[SCM] PostGIS branch master updated. 3.6.0rc2-649-g1b74f2013
git at osgeo.org
git at osgeo.org
Sun Jun 21 11:30:40 PDT 2026
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "PostGIS".
The branch, master has been updated
via 1b74f201374035483b1d9e53f0fa4d981349f78b (commit)
from c4bae99fc25aabfc512dd0c14f1474b66b3e7730 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit 1b74f201374035483b1d9e53f0fa4d981349f78b
Author: Darafei Praliaskouski <me at komzpa.net>
Date: Sun Jun 21 22:11:27 2026 +0400
Allow custom shp2pgsql feature id column
Add shp2pgsql -f/--feature-id-column so callers can choose the generated feature id column name instead of always using gid.
Validate the configured column name before generating SQL, and keep escaping source gid attributes to __gid so default pgsql2shp round trips do not silently drop them.
Closes #3033
Closes https://github.com/postgis/postgis/pull/1022
diff --git a/NEWS b/NEWS
index 7f37d0986..2f67dfa1e 100644
--- a/NEWS
+++ b/NEWS
@@ -21,11 +21,12 @@ To take advantage of all postgis_sfcgal extension features SFCGAL 2.3+ is needed
* New Features *
- - #1124, #2935, #4658, #4659, [loader] Rework loader arguments: shp2pgsql can
- create UNLOGGED tables, shp2pgsql --drop-table can emit DROP TABLE
- before prepare output, raster2pgsql/shp2pgsql expose loader actions
- as long options, both loaders accept long aliases for existing
- options, and --if-not-exists makes creation actions idempotent
+ - #1124, #2935, #3033, #4658, #4659, [loader] Rework loader arguments:
+ shp2pgsql can create UNLOGGED tables and choose the feature id column
+ name, shp2pgsql --drop-table can emit DROP TABLE before prepare
+ output, raster2pgsql/shp2pgsql expose loader actions as long options,
+ both loaders accept long aliases for existing options, and
+ --if-not-exists makes creation actions idempotent
(Darafei Praliaskouski)
- #4208, Add single-geometry variants of ST_MaxDistance and ST_LongestLine
(Darafei Praliaskouski)
diff --git a/doc/man/shp2pgsql.1 b/doc/man/shp2pgsql.1
index 2895b5c36..2ff16973d 100644
--- a/doc/man/shp2pgsql.1
+++ b/doc/man/shp2pgsql.1
@@ -90,6 +90,9 @@ lat/lon data. At the moment the only spatial reference supported is 4326.
\fB\-g\fR, \fB\-\-geometry\-column\fR <\fIgeometry_column\fR>
Specify the name of the geometry column (mostly useful in append mode).
.TP
+\fB\-f\fR, \fB\-\-feature\-id\-column\fR <\fIfid_column\fR>
+Specify the name of the feature id column. The default is gid.
+.TP
\fB\-\-create\-index\fR
Create a spatial index on the geometry column.
.TP
diff --git a/doc/using_postgis_dataman.xml b/doc/using_postgis_dataman.xml
index d7ed22913..b902f54af 100644
--- a/doc/using_postgis_dataman.xml
+++ b/doc/using_postgis_dataman.xml
@@ -1966,6 +1966,18 @@ COMMIT;</programlisting>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>-f <fid_column></option></term>
+ <term><option>--feature-id-column <fid_column></option></term>
+ <listitem>
+ <para>
+ Specify the name of the feature id column. The default is <literal>gid</literal>.
+ The value must be a simple PostgreSQL identifier and cannot use a
+ PostgreSQL system column name.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>-s [<FROM_SRID>:]<SRID></option></term>
<listitem>
diff --git a/loader/README.shp2pgsql b/loader/README.shp2pgsql
index 0fd7685d8..9f7a93ee8 100644
--- a/loader/README.shp2pgsql
+++ b/loader/README.shp2pgsql
@@ -78,6 +78,9 @@ OPTIONS
Specify the name of the geometry column (mostly useful in append
mode).
+ -f, --feature-id-column <fid_column>
+ Specify the name of the feature id column. The default is gid.
+
--create-index
Create a spatial index on the geometry column.
diff --git a/loader/cunit/cu_shp2pgsql.c b/loader/cunit/cu_shp2pgsql.c
index c43f5ac55..a674b1a26 100644
--- a/loader/cunit/cu_shp2pgsql.c
+++ b/loader/cunit/cu_shp2pgsql.c
@@ -20,6 +20,8 @@ void test_ShpLoaderDestroy(void);
void test_ShpLoaderGetSQLHeader_drop_prepare(void);
void test_ShpLoaderGetSQLHeader_if_not_exists_table_modifier(void);
void test_ShpLoaderGetSQLFooter_if_not_exists_index_modifier(void);
+void test_ShpLoaderFIDColumnSQL(void);
+void test_ShpLoaderRejectsInvalidFIDColumn(void);
void test_ShpLoaderOpenShapeRejectsZip(void);
SHPLOADERCONFIG *loader_config;
@@ -49,6 +51,9 @@ CU_pSuite register_shp2pgsql_suite(void)
(NULL == CU_add_test(pSuite,
"test_ShpLoaderGetSQLFooter_if_not_exists_index_modifier()",
test_ShpLoaderGetSQLFooter_if_not_exists_index_modifier)) ||
+ (NULL == CU_add_test(pSuite, "test_ShpLoaderFIDColumnSQL()", test_ShpLoaderFIDColumnSQL)) ||
+ (NULL ==
+ CU_add_test(pSuite, "test_ShpLoaderRejectsInvalidFIDColumn()", test_ShpLoaderRejectsInvalidFIDColumn)) ||
(NULL == CU_add_test(pSuite, "test_ShpLoaderOpenShapeRejectsZip()", test_ShpLoaderOpenShapeRejectsZip)))
{
CU_cleanup_registry();
@@ -148,7 +153,7 @@ test_ShpLoaderGetSQLHeader_if_not_exists_table_modifier(void)
CU_ASSERT_PTR_NOT_NULL(header);
CU_ASSERT_PTR_NOT_NULL(strstr(header,
"CREATE TABLE IF NOT EXISTS \"loadedshp\" "
- "(gid serial PRIMARY KEY,\n"
+ "(\"gid\" serial PRIMARY KEY,\n"
"\"the_geom\" geometry(POINT,0));"));
CU_ASSERT_PTR_NULL(strstr(header, "AddGeometryColumn"));
CU_ASSERT_PTR_NULL(strstr(header, "ALTER TABLE"));
@@ -178,6 +183,66 @@ test_ShpLoaderGetSQLFooter_if_not_exists_index_modifier(void)
ShpLoaderDestroy(loader_state);
}
+void
+test_ShpLoaderFIDColumnSQL(void)
+{
+ SHPLOADERCONFIG config;
+ SHPLOADERSTATE *state;
+ char *header = NULL;
+
+ set_loader_config_defaults(&config);
+ config.table = "loadedshp";
+ config.readshape = 0;
+
+ state = ShpLoaderCreate(&config);
+ CU_ASSERT_EQUAL_FATAL(ShpLoaderGetSQLHeader(state, &header), SHPLOADEROK);
+ CU_ASSERT_PTR_NOT_NULL_FATAL(header);
+ CU_ASSERT_PTR_NOT_NULL(strstr(header, "CREATE TABLE \"loadedshp\" (\"gid\" serial"));
+ CU_ASSERT_PTR_NOT_NULL(strstr(header, "ADD PRIMARY KEY (\"gid\")"));
+ free(header);
+ header = NULL;
+ ShpLoaderDestroy(state);
+
+ config.fid_col = "fid";
+ state = ShpLoaderCreate(&config);
+ CU_ASSERT_EQUAL_FATAL(ShpLoaderGetSQLHeader(state, &header), SHPLOADEROK);
+ CU_ASSERT_PTR_NOT_NULL_FATAL(header);
+ CU_ASSERT_PTR_NOT_NULL(strstr(header, "CREATE TABLE \"loadedshp\" (\"fid\" serial"));
+ CU_ASSERT_PTR_NOT_NULL(strstr(header, "ADD PRIMARY KEY (\"fid\")"));
+ free(header);
+ ShpLoaderDestroy(state);
+
+ free(config.encoding);
+}
+
+void
+test_ShpLoaderRejectsInvalidFIDColumn(void)
+{
+ SHPLOADERCONFIG config;
+ SHPLOADERSTATE *state;
+ char *header = NULL;
+
+ set_loader_config_defaults(&config);
+ config.table = "loadedshp";
+ config.readshape = 0;
+ config.fid_col = "bad\"fid";
+
+ state = ShpLoaderCreate(&config);
+ CU_ASSERT_EQUAL(ShpLoaderGetSQLHeader(state, &header), SHPLOADERERR);
+ CU_ASSERT_PTR_NULL(header);
+ CU_ASSERT_PTR_NOT_NULL(strstr(state->message, "Invalid feature id column name"));
+ ShpLoaderDestroy(state);
+
+ config.fid_col = "ctid";
+ state = ShpLoaderCreate(&config);
+ CU_ASSERT_EQUAL(ShpLoaderGetSQLHeader(state, &header), SHPLOADERERR);
+ CU_ASSERT_PTR_NULL(header);
+ CU_ASSERT_PTR_NOT_NULL(strstr(state->message, "Invalid feature id column name"));
+ ShpLoaderDestroy(state);
+
+ free(config.encoding);
+}
+
void
test_ShpLoaderOpenShapeRejectsZip(void)
{
diff --git a/loader/shp2pgsql-cli.c b/loader/shp2pgsql-cli.c
index 13690e91f..a7c6db37d 100644
--- a/loader/shp2pgsql-cli.c
+++ b/loader/shp2pgsql-cli.c
@@ -33,6 +33,7 @@ static const ShpLoaderLongOption long_option_aliases[] = {
{"dimensionality", 't', 1},
{"dump-format", 'D', 0},
{"encoding", 'W', 1},
+ {"feature-id-column", 'f', 1},
{"force-int4", 'i', 0},
{"geography", 'G', 0},
{"geometry-column", 'g', 1},
@@ -125,6 +126,9 @@ usage()
printf(
_(" -g, --geometry-column <geocolumn> Specify the name of the geometry/geography column\n"
" (mostly useful in append mode).\n"));
+ printf(
+ _(" -f, --feature-id-column <fidcolumn> Specify the name of the feature id column\n"
+ " (default: \"" FID_DEFAULT "\").\n"));
printf(_(" -D, --dump-format Use postgresql dump format (defaults to SQL insert statements).\n"));
printf(_( " -e Execute each statement individually, do not use a transaction.\n"
" Not compatible with -D.\n" ));
@@ -283,7 +287,7 @@ main (int argc, char **argv)
}
else
{
- c = pgis_getopt(argc, argv, "-?acdeg:ikm:nps:t:uwDGIN:ST:W:X:Z");
+ c = pgis_getopt(argc, argv, "-?acdef:g:ikm:nps:t:uwDGIN:ST:W:X:Z");
if (c == EOF)
break;
@@ -344,6 +348,9 @@ main (int argc, char **argv)
case 'g':
config->geo_col = pgis_optarg;
break;
+ case 'f':
+ config->fid_col = pgis_optarg;
+ break;
case 'm':
config->column_map_filename = pgis_optarg;
break;
diff --git a/loader/shp2pgsql-core.c b/loader/shp2pgsql-core.c
index 394d5dd66..a62e8113b 100644
--- a/loader/shp2pgsql-core.c
+++ b/loader/shp2pgsql-core.c
@@ -773,6 +773,78 @@ strtolower(char *s)
s[j] = tolower(s[j]);
}
+static const char *
+ShpLoaderFIDColumn(const SHPLOADERSTATE *state)
+{
+ return state->config->fid_col ? state->config->fid_col : FID_DEFAULT;
+}
+
+static int
+ShpLoaderNameMatchesAny(const char *name, const char *const *names, size_t num_names, int case_sensitive)
+{
+ size_t i;
+
+ for (i = 0; i < num_names; i++)
+ {
+ if (case_sensitive ? !strcmp(name, names[i]) : !strcasecmp(name, names[i]))
+ return LW_TRUE;
+ }
+
+ return LW_FALSE;
+}
+
+static int
+ShpLoaderFIDColumnIsSystemColumn(const char *name)
+{
+ static const char *const names[] = {"tableoid", "cmin", "cmax", "xmin", "xmax", "oid", "ctid"};
+
+ return ShpLoaderNameMatchesAny(name, names, sizeof(names) / sizeof(names[0]), LW_FALSE);
+}
+
+static int
+ShpLoaderFIDColumnIsValid(const char *name)
+{
+ size_t i;
+
+ if (!name || !name[0] || ShpLoaderFIDColumnIsSystemColumn(name))
+ return LW_FALSE;
+
+ if (!isalpha((unsigned char)name[0]) && name[0] != '_')
+ return LW_FALSE;
+
+ for (i = 1; name[i]; i++)
+ {
+ if (!isalnum((unsigned char)name[i]) && name[i] != '_')
+ return LW_FALSE;
+ }
+
+ return LW_TRUE;
+}
+
+static int
+ShpLoaderValidateFIDColumn(SHPLOADERSTATE *state)
+{
+ const char *fid_col = ShpLoaderFIDColumn(state);
+
+ if (ShpLoaderFIDColumnIsValid(fid_col))
+ return SHPLOADEROK;
+
+ snprintf(state->message, SHPLOADERMSGLEN, _("Invalid feature id column name \"%s\""), fid_col ? fid_col : "");
+ return SHPLOADERERR;
+}
+
+static int
+ShpLoaderFieldNameNeedsEscape(const SHPLOADERSTATE *state, const char *name)
+{
+ static const char *const reserved_names[] = {
+ FID_DEFAULT, "tableoid", "cmin", "cmax", "xmin", "xmax", "primary", "oid", "ctid"};
+ const char *fid_col = ShpLoaderFIDColumn(state);
+
+ return name[0] == '_' || !strcmp(name, fid_col) ||
+ ShpLoaderNameMatchesAny(
+ name, reserved_names, sizeof(reserved_names) / sizeof(reserved_names[0]), LW_TRUE);
+}
+
static void
pgtype_typmod_name(const SHPLOADERSTATE *state, char *buf, size_t len)
{
@@ -803,9 +875,11 @@ append_qualified_table(stringbuffer_t *sb, const char *schema, const char *table
static void
append_primary_key_ddl(const SHPLOADERSTATE *state, stringbuffer_t *sb)
{
+ const char *fid_col = ShpLoaderFIDColumn(state);
+
stringbuffer_aprintf(sb, "ALTER TABLE ");
append_qualified_table(sb, state->config->schema, state->config->table);
- stringbuffer_aprintf(sb, " ADD PRIMARY KEY (gid);\n");
+ stringbuffer_aprintf(sb, " ADD PRIMARY KEY (\"%s\");\n", fid_col);
if (state->config->idxtablespace != NULL)
{
@@ -832,6 +906,7 @@ set_loader_config_defaults(SHPLOADERCONFIG *config)
config->table = NULL;
config->schema = NULL;
config->geo_col = NULL;
+ config->fid_col = NULL;
config->shp_file = NULL;
config->dump_format = 0;
config->simple_geometries = 0;
@@ -985,6 +1060,9 @@ ShpLoaderOpenShape(SHPLOADERSTATE *state)
DBFFieldType type = FTInvalid;
char *utf8str;
+ if (ShpLoaderValidateFIDColumn(state) != SHPLOADEROK)
+ return SHPLOADERERR;
+
if (shp_loader_is_zip_archive(state->config->shp_file))
{
snprintf(state->message,
@@ -1305,20 +1383,12 @@ ShpLoaderOpenShape(SHPLOADERSTATE *state)
strtolower(name);
/*
- * Escape names starting with the
- * escape char (_), those named 'gid'
- * or after pgsql reserved attribute names
+ * Keep generated loader columns and PostgreSQL system columns out of
+ * the DBF attribute namespace. Always escape source "gid" columns too:
+ * pgsql2shp treats gid as a loader id by default, so preserving the old
+ * "__gid" mapping keeps default round trips lossless even with -f.
*/
- if (name[0] == '_' ||
- ! strcmp(name, "gid") ||
- ! strcmp(name, "tableoid") ||
- ! strcmp(name, "cmin") ||
- ! strcmp(name, "cmax") ||
- ! strcmp(name, "xmin") ||
- ! strcmp(name, "xmax") ||
- ! strcmp(name, "primary") ||
- ! strcmp(name, "oid") ||
- ! strcmp(name, "ctid"))
+ if (ShpLoaderFieldNameNeedsEscape(state, name))
{
char tmp[MAXFIELDNAMELEN] = "__";
memcpy(tmp+2, name, MAXFIELDNAMELEN-2);
@@ -1425,9 +1495,15 @@ ShpLoaderGetSQLHeader(SHPLOADERSTATE *state, char **strheader)
stringbuffer_t *sb;
char *ret;
int j;
+ const char *fid_col;
const int use_transaction =
state->config->plan.transaction == LOADER_TRANSACTION_ON && plan_has_transactional_work(&state->config->plan);
+ if (ShpLoaderValidateFIDColumn(state) != SHPLOADEROK)
+ return SHPLOADERERR;
+
+ fid_col = ShpLoaderFIDColumn(state);
+
/* Create the stringbuffer containing the header; we use this API as it's easier
for handling string resizing during append */
sb = stringbuffer_create();
@@ -1476,7 +1552,7 @@ ShpLoaderGetSQLHeader(SHPLOADERSTATE *state, char **strheader)
state->config->unlogged ? "UNLOGGED " : "",
if_not_exists ? "IF NOT EXISTS " : "");
append_qualified_table(sb, state->config->schema, state->config->table);
- stringbuffer_aprintf(sb, " (gid serial%s", if_not_exists ? " PRIMARY KEY" : "");
+ stringbuffer_aprintf(sb, " (\"%s\" serial%s", fid_col, if_not_exists ? " PRIMARY KEY" : "");
if (if_not_exists && state->config->idxtablespace != NULL)
{
diff --git a/loader/shp2pgsql-core.h b/loader/shp2pgsql-core.h
index b106e2cde..95ca8866f 100644
--- a/loader/shp2pgsql-core.h
+++ b/loader/shp2pgsql-core.h
@@ -71,6 +71,7 @@
*/
#define GEOMETRY_DEFAULT "geom"
#define GEOGRAPHY_DEFAULT "geog"
+#define FID_DEFAULT "gid"
/*
* Default character encoding
@@ -94,6 +95,9 @@ typedef struct shp_loader_config
/* geometry/geography column name specified by the user, may be null. */
char *geo_col;
+ /* feature id column name specified by the user, may be null. */
+ char *fid_col;
+
/* the shape file (without the .shp extension) */
char *shp_file;
diff --git a/regress/loader/FIDColumn.dbf b/regress/loader/FIDColumn.dbf
new file mode 100644
index 000000000..9edcc7d04
Binary files /dev/null and b/regress/loader/FIDColumn.dbf differ
diff --git a/regress/loader/FIDColumn.opts b/regress/loader/FIDColumn.opts
new file mode 100644
index 000000000..cc8a4fb05
--- /dev/null
+++ b/regress/loader/FIDColumn.opts
@@ -0,0 +1,5 @@
+# Reuse the CharNoWidth DBF layout to verify that a custom feature id column
+# changes the loader primary key but still escapes a source DBF column mapped to
+# gid. pgsql2shp treats gid as a loader id by default, so __gid preserves that
+# source attribute through default dump/load round trips.
+--feature-id-column fid -m {regdir}/loader/FIDColumn_mapping.txt
diff --git a/regress/loader/FIDColumn.select.expected b/regress/loader/FIDColumn.select.expected
new file mode 100644
index 000000000..8c9e55cfa
--- /dev/null
+++ b/regress/loader/FIDColumn.select.expected
@@ -0,0 +1,2 @@
+fid
+__gid
diff --git a/regress/loader/FIDColumn.select.sql b/regress/loader/FIDColumn.select.sql
new file mode 100644
index 000000000..849c569ea
--- /dev/null
+++ b/regress/loader/FIDColumn.select.sql
@@ -0,0 +1,6 @@
+SET CLIENT_ENCODING to UTF8;
+SELECT column_name
+FROM information_schema.columns
+WHERE table_name = 'loadedshp'
+ AND column_name IN ('fid', 'gid', '__gid')
+ORDER BY ordinal_position;
diff --git a/regress/loader/FIDColumn_mapping.txt b/regress/loader/FIDColumn_mapping.txt
new file mode 100644
index 000000000..2ac8f55e4
--- /dev/null
+++ b/regress/loader/FIDColumn_mapping.txt
@@ -0,0 +1 @@
+gid STATEFP
diff --git a/regress/loader/tests.mk b/regress/loader/tests.mk
index 40941b7e6..b40949bb8 100644
--- a/regress/loader/tests.mk
+++ b/regress/loader/tests.mk
@@ -42,3 +42,4 @@ TESTS += \
$(top_srcdir)/regress/loader/TestSkipANALYZE \
$(top_srcdir)/regress/loader/TestANALYZE \
$(top_srcdir)/regress/loader/CharNoWidth \
+ $(top_srcdir)/regress/loader/FIDColumn \
-----------------------------------------------------------------------
Summary of changes:
NEWS | 11 ++-
doc/man/shp2pgsql.1 | 3 +
doc/using_postgis_dataman.xml | 12 +++
loader/README.shp2pgsql | 3 +
loader/cunit/cu_shp2pgsql.c | 67 +++++++++++++-
loader/shp2pgsql-cli.c | 9 +-
loader/shp2pgsql-core.c | 106 +++++++++++++++++++---
loader/shp2pgsql-core.h | 4 +
regress/loader/{CharNoWidth.dbf => FIDColumn.dbf} | Bin
regress/loader/FIDColumn.opts | 5 +
regress/loader/FIDColumn.select.expected | 2 +
regress/loader/FIDColumn.select.sql | 6 ++
regress/loader/FIDColumn_mapping.txt | 1 +
regress/loader/tests.mk | 1 +
14 files changed, 208 insertions(+), 22 deletions(-)
copy regress/loader/{CharNoWidth.dbf => FIDColumn.dbf} (100%)
create mode 100644 regress/loader/FIDColumn.opts
create mode 100644 regress/loader/FIDColumn.select.expected
create mode 100644 regress/loader/FIDColumn.select.sql
create mode 100644 regress/loader/FIDColumn_mapping.txt
hooks/post-receive
--
PostGIS
More information about the postgis-tickets
mailing list