Logo

Export PostgreSQL/PostGIS Data with Attachments to File Geodatabase and Publish to ArcGIS Enterprise


 

Export PostgreSQL/PostGIS Data with Attachments to File Geodatabase and Publish to ArcGIS Enterprise

Introduction

In many GIS workflows, spatial data is stored inside PostgreSQL/PostGIS while image files are stored separately on disk. A common requirement is to:

  • Export spatial features from PostGIS

  • Include image attachments

  • Create a File Geodatabase (.gdb)

  • Zip the geodatabase

  • Publish it to ArcGIS Enterprise as a hosted feature layer with attachments

This tutorial explains the complete workflow using ArcPy in ArcGIS Pro.


Architecture Overview

Source Database

PostgreSQL/PostGIS

Main Feature Table

wb_main

Contains:

  • Spatial geometry

  • Waterbody attributes

  • Unique identifier

Attachment Table

wb_photo

Contains:

  • igis_id

  • photoname

  • image metadata

Image Folder

H:\aura\photos

Contains actual image files:

1.jpg
2.jpg
3.jpg

Final Output

The script generates:

waterbody.gdb

Containing:

wb_main
wb_main__ATTACH
wb_main__ATTACHREL

and:

waterbody.zip

which can be published to ArcGIS Enterprise.


Prerequisites

Software Required

  • ArcGIS Pro

  • PostgreSQL

  • PostGIS Extension

  • ArcPy


Important Database Requirement

ArcGIS Pro requires a real geometry column.

Latitude and longitude fields alone are NOT enough.

Your table must contain:

geom geometry(Point,4326)

Create Geometry Column in PostGIS

Step 1 — Add Geometry Column

ALTER TABLE wb_main
ADD COLUMN geom geometry(Point,4326);

Step 2 — Populate Geometry

UPDATE wb_main
SET geom = ST_SetSRID(
    ST_MakePoint(longitude, latitude),
    4326
)
WHERE longitude IS NOT NULL
AND latitude IS NOT NULL;

Step 3 — Create Spatial Index

CREATE INDEX idx_wb_main_geom
ON wb_main
USING GIST(geom);

Verify in ArcGIS Pro

After reconnecting the .sde connection:

aura.public.wb_main

must appear as:

Feature Class

NOT:

Database Table

Create Database Connection (.sde)

Inside ArcGIS Pro:

Catalog Pane
→ Databases
→ New Database Connection

Configuration:

SettingValue
PlatformPostgreSQL
Instancelocalhost,5432
AuthenticationDatabase Authentication
Usernamepostgres
Databaseaura

Save the connection.

Example:

H:\arcpro\gdb_bild\localhost.sde

Final ArcPy Script

import arcpy
import os
import csv
import shutil

# =========================
# CONFIG
# =========================

# SDE Connection
sde_conn = r"H:\arcpro\gdb_bild\localhost.sde"

# Feature Class
feature_class = os.path.join(
    sde_conn,
    "aura.public.wb_main"
)

# Photo Table
photo_table = os.path.join(
    sde_conn,
    "aura.public.wb_photo"
)

# Folder containing actual image files
image_folder = r"H:\aura\photos"

# Output Folder
output_folder = r"H:\aura\output"

# Output GDB Name
gdb_name = "waterbody.gdb"

# ZIP File Name
zip_name = "waterbody"

# Temporary CSV for attachment mapping
match_table_csv = os.path.join(
    output_folder,
    "attachment_match.csv"
)

# =========================
# CREATE OUTPUT FOLDER
# =========================

if not os.path.exists(output_folder):
    os.makedirs(output_folder)

# =========================
# CREATE FILE GDB
# =========================

gdb_path = os.path.join(output_folder, gdb_name)

# Delete old GDB if exists
if arcpy.Exists(gdb_path):

    arcpy.management.Delete(gdb_path)

    print("Old GDB deleted.")

# Create new GDB
arcpy.management.CreateFileGDB(
    output_folder,
    gdb_name
)

print("File GDB created.")

# =========================
# EXPORT FEATURE CLASS
# =========================

output_fc = os.path.join(
    gdb_path,
    "wb_main"
)

# Export feature class
arcpy.conversion.FeatureClassToFeatureClass(
    feature_class,
    gdb_path,
    "wb_main"
)

print("Feature class exported.")

# =========================
# ADD GLOBAL IDS
# =========================

arcpy.management.AddGlobalIDs(output_fc)

print("Global IDs added.")

# =========================
# ENABLE ATTACHMENTS
# =========================

arcpy.management.EnableAttachments(output_fc)

print("Attachments enabled.")

# =========================
# BUILD MATCH TABLE CSV
# =========================

with open(match_table_csv, "w", newline="") as csvfile:

    writer = csv.writer(csvfile)

    # CSV Header
    writer.writerow([
        "igis_id",
        "photo_path"
    ])

    # Read photo table
    with arcpy.da.SearchCursor(
        photo_table,
        ["igis_id", "photoname"]
    ) as cursor:

        for row in cursor:

            igis_id = row[0]
            photoname = row[1]

            # Build image path
            filepath = os.path.join(
                image_folder,
                photoname
            )

            print(f"Checking: {filepath}")

            # Check image exists
            if os.path.exists(filepath):

                writer.writerow([
                    igis_id,
                    filepath
                ])

                print(f"Added: {filepath}")

            else:

                print(f"Missing file: {filepath}")

print("Attachment match table created.")

# =========================
# ADD ATTACHMENTS
# =========================

arcpy.management.AddAttachments(
    in_dataset=output_fc,
    in_join_field="igis_id",
    in_match_table=match_table_csv,
    in_match_join_field="igis_id",
    in_match_path_field="photo_path"
)

print("Attachments added successfully.")

# =========================
# DELETE TEMP CSV
# =========================

if os.path.exists(match_table_csv):

    os.remove(match_table_csv)

    print("Temporary CSV deleted.")

# =========================
# CREATE ZIP FILE
# =========================

zip_output = os.path.join(
    output_folder,
    zip_name
)

# Delete old zip if exists
if os.path.exists(zip_output + ".zip"):

    os.remove(zip_output + ".zip")

    print("Old ZIP deleted.")

# Create ZIP
shutil.make_archive(
    zip_output,
    'zip',
    output_folder,
    gdb_name
)

print("ZIP file created.")

# =========================
# FINAL OUTPUT
# =========================

print("DONE")

print(f"GDB Path: {gdb_path}")

print(f"ZIP Path: {zip_output}.zip")

Run Script in ArcGIS Pro

Option 1 — Python Window

Inside ArcGIS Pro:

View
→ Python Window

Paste and run the script.


Option 2 — Notebook

Insert
→ New Notebook

Run script cells.


Publishing to ArcGIS Enterprise

IMPORTANT

Do NOT upload the ZIP directly from Portal.

Recommended workflow:

ArcGIS Pro
→ Share As Web Layer

Publishing Steps

Step 1

Add:

wb_main

from the generated GDB into the map.


Step 2

Verify attachments work locally.

Click features and confirm images appear.


Step 3

Enable unique numeric IDs.

Map Properties
→ General
→ Allow assignment of unique numeric IDs

Step 4

Share:

Sharing
→ Share As Web Layer

Step 5

Configuration:

SettingValue
Layer TypeFeature
Data OptionCopy all data
Enable AttachmentsYes

Common Errors

ERROR 000732

Dataset does not exist or is not supported

Cause:

  • Table is not spatial

  • Missing geometry column

Fix:

  • Create PostGIS geometry column


ERROR 00374

Unique numeric IDs are not assigned

Fix:

Map Properties
→ Enable unique numeric IDs

ERROR 00231

Layer's data source must be registered with the server

Fix:

Use:

Copy all data

instead of:

Reference registered data

Final Result

You now have:

  • PostgreSQL/PostGIS spatial data

  • ArcGIS Pro integration

  • File Geodatabase export

  • Image attachments

  • ZIP packaging

  • ArcGIS Enterprise publishing workflow

with fully working hosted feature layer attachments.


Output Structure

output/
│
├── waterbody.gdb
│   ├── wb_main
│   ├── wb_main__ATTACH
│   └── wb_main__ATTACHREL
│
└── waterbody.zip

Conclusion

This workflow provides a complete production-ready pipeline for exporting PostGIS data with image attachments into ArcGIS-compatible File Geodatabases and publishing them to ArcGIS Enterprise.

The same approach can also be extended for:

  • Survey applications

  • Asset management systems

  • Environmental monitoring

  • Utility inspection systems

  • Waterbody inventory projects

  • Mobile GIS workflows