[{"data":1,"prerenderedAt":1932},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fimport-layer-into-postgis-pyqgis":3},{"id":4,"title":5,"body":6,"description":1921,"extension":1922,"meta":1923,"navigation":241,"path":1928,"seo":1929,"stem":1930,"__hash__":1931},"docs\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fimport-layer-into-postgis-pyqgis\u002Findex.md","Import a Layer into PostGIS with PyQGIS",{"type":7,"value":8,"toc":1906},"minimark",[9,13,17,31,168,173,190,194,201,467,498,502,509,569,847,861,865,868,1046,1075,1079,1082,1122,1130,1134,1137,1213,1558,1563,1567,1570,1607,1628,1632,1635,1752,1757,1761,1785,1789,1825,1829,1835,1839,1849,1858,1864,1870,1874,1902],[10,11,5],"h1",{"id":12},"import-a-layer-into-postgis-with-pyqgis",[14,15,16],"p",{},"Moving data into PostGIS is the step that turns a folder of files into a shared, queryable, multi-user dataset. QGIS can import any layer it can read — shapefiles, GeoPackages, CSVs, web services, memory layers — into a PostGIS table, creating the table with the right columns, geometry type and CRS. Doing it from Python makes imports repeatable: the same naming, keys, indexes and checks every time, for one layer or a hundred.",[14,18,19,20,25,26,30],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002F","PostGIS & Database Workflows",". It imports a layer with Processing, controls the table definition, uses the connections API for finer control, creates indexes and statistics, imports a folder of files, and verifies the result. Appending to tables that already exist is covered in ",[21,27,29],{"href":28},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fappend-features-to-postgis-table-pyqgis\u002F","appending features to a PostGIS table",".",[14,32,33],{},[34,35,40,44,48,55,72,81,90,96,101,105,112,120,126,131,135,139,143,147,151,157,162,165],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 234","img","A source layer imported into a PostGIS table with decisions about schema, name, primary key, geometry column, type, CRS and overwrite, followed by indexing","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"What an import decides",[45,46,47],"desc",{},"An import turns a source layer into a PostGIS table. It decides the schema and table name, the primary key column, the geometry column name and type, the CRS stored in the geometry column, how field names are converted, and whether an existing table is overwritten. After loading, a spatial index and table statistics make the table fast to query.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","234","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"pgImportArrow","0 0 10 10","8","5","7","auto-start-reverse",[68,69],"path",{"d":70,"fill":71},"M0 0 L10 5 L0 10 z","#2f3b35",[73,74,80],"text",{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Source layer → well-defined table",[49,82],{"x":83,"y":84,"width":85,"height":86,"rx":63,"fill":87,"stroke":88,"style":89},"24","70","160","130","#fffdf7","#59645f","stroke-width:2",[73,91,95],{"x":92,"y":93,"style":94,"fill":78,"textAnchor":79},"104","112.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","source",[73,97,100],{"x":92,"y":98,"style":99,"fill":88,"textAnchor":79},"138.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","shp, gpkg, csv,",[73,102,104],{"x":92,"y":103,"style":99,"fill":88,"textAnchor":79},"164.78","memory, WFS",[106,107],"line",{"x1":108,"y1":109,"x2":110,"y2":109,"stroke":71,"style":111},"184","135","224","stroke-width:1.8;marker-end:url(#pgImportArrow)",[49,113],{"x":114,"y":115,"width":116,"height":117,"rx":63,"fill":118,"stroke":119,"style":89},"228","52","290","166","#eef7f4","#0f766e",[73,121,125],{"x":122,"y":123,"style":124,"fill":119,"textAnchor":79},"373","78.6","text-anchor:middle;font-size:11.0px;font-family:sans-serif;font-weight:bold","table definition",[73,127,130],{"x":122,"y":128,"style":129,"fill":71,"textAnchor":79},"102.6","text-anchor:middle;font-size:10.0px;font-family:sans-serif","schema.table",[73,132,134],{"x":122,"y":133,"style":129,"fill":71,"textAnchor":79},"126.6","primary key",[73,136,138],{"x":122,"y":137,"style":129,"fill":71,"textAnchor":79},"150.6","geom column + type",[73,140,142],{"x":122,"y":141,"style":129,"fill":71,"textAnchor":79},"174.6","CRS (SRID)",[73,144,146],{"x":122,"y":145,"style":129,"fill":71,"textAnchor":79},"198.6","overwrite?",[106,148],{"x1":149,"y1":109,"x2":150,"y2":109,"stroke":71,"style":111},"518","558",[49,152],{"x":153,"y":84,"width":154,"height":86,"rx":63,"fill":155,"stroke":156,"style":89},"562","174","#e8efe6","#15803d",[73,158,161],{"x":159,"y":93,"style":94,"fill":160,"textAnchor":79},"649","#166534","after load",[73,163,164],{"x":159,"y":98,"style":99,"fill":88,"textAnchor":79},"spatial index",[73,166,167],{"x":159,"y":103,"style":99,"fill":88,"textAnchor":79},"ANALYZE",[169,170,172],"h2",{"id":171},"prerequisites","Prerequisites",[174,175,176,180,187],"ul",{},[177,178,179],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[177,181,182,183,30],{},"A PostgreSQL database with the PostGIS extension and a saved QGIS connection, as set up in ",[21,184,186],{"href":185},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fconnect-to-postgis-database-pyqgis\u002F","connecting to a PostGIS database",[177,188,189],{},"A database role allowed to create tables in the target schema.",[169,191,193],{"id":192},"import-with-processing","Import with Processing",[14,195,196,200],{},[197,198,199],"code",{},"native:importintopostgis"," (shown as \"Export to PostgreSQL\" in the toolbox) takes a layer and a stored connection name, and creates the table.",[202,203,208],"pre",{"className":204,"code":205,"language":206,"meta":207,"style":207},"language-python shiki shiki-themes github-dark","import processing\nfrom qgis.core import QgsProject, QgsVectorLayer\n\nsrc = QgsVectorLayer(\"\u002Fdata\u002Finbox\u002Fhydrants.gpkg|layername=hydrants\", \"hydrants\", \"ogr\")\nprocessing.run(\"native:importintopostgis\", {\n    \"INPUT\": src,\n    \"DATABASE\": \"assets_db\",          # name of the saved QGIS connection\n    \"SCHEMA\": \"water\",\n    \"TABLENAME\": \"hydrants\",\n    \"PRIMARY_KEY\": \"hydrant_pk\",\n    \"GEOMETRY_COLUMN\": \"geom\",\n    \"ENCODING\": \"UTF-8\",\n    \"OVERWRITE\": True,\n    \"CREATEINDEX\": True,\n    \"LOWERCASE_NAMES\": True,\n    \"DROP_STRING_LENGTH\": False,\n    \"FORCE_SINGLEPART\": False,\n})\nprint(\"imported\", src.featureCount(), \"features into water.hydrants\")\n","python","",[197,209,210,222,236,243,273,285,294,313,327,339,352,365,378,392,404,416,429,441,447],{"__ignoreMap":207},[211,212,214,218],"span",{"class":106,"line":213},1,[211,215,217],{"class":216},"snl16","import",[211,219,221],{"class":220},"s95oV"," processing\n",[211,223,225,228,231,233],{"class":106,"line":224},2,[211,226,227],{"class":216},"from",[211,229,230],{"class":220}," qgis.core ",[211,232,217],{"class":216},[211,234,235],{"class":220}," QgsProject, QgsVectorLayer\n",[211,237,239],{"class":106,"line":238},3,[211,240,242],{"emptyLinePlaceholder":241},true,"\n",[211,244,246,249,252,255,259,262,265,267,270],{"class":106,"line":245},4,[211,247,248],{"class":220},"src ",[211,250,251],{"class":216},"=",[211,253,254],{"class":220}," QgsVectorLayer(",[211,256,258],{"class":257},"sU2Wk","\"\u002Fdata\u002Finbox\u002Fhydrants.gpkg|layername=hydrants\"",[211,260,261],{"class":220},", ",[211,263,264],{"class":257},"\"hydrants\"",[211,266,261],{"class":220},[211,268,269],{"class":257},"\"ogr\"",[211,271,272],{"class":220},")\n",[211,274,276,279,282],{"class":106,"line":275},5,[211,277,278],{"class":220},"processing.run(",[211,280,281],{"class":257},"\"native:importintopostgis\"",[211,283,284],{"class":220},", {\n",[211,286,288,291],{"class":106,"line":287},6,[211,289,290],{"class":257},"    \"INPUT\"",[211,292,293],{"class":220},": src,\n",[211,295,297,300,303,306,309],{"class":106,"line":296},7,[211,298,299],{"class":257},"    \"DATABASE\"",[211,301,302],{"class":220},": ",[211,304,305],{"class":257},"\"assets_db\"",[211,307,308],{"class":220},",          ",[211,310,312],{"class":311},"sjoCn","# name of the saved QGIS connection\n",[211,314,316,319,321,324],{"class":106,"line":315},8,[211,317,318],{"class":257},"    \"SCHEMA\"",[211,320,302],{"class":220},[211,322,323],{"class":257},"\"water\"",[211,325,326],{"class":220},",\n",[211,328,330,333,335,337],{"class":106,"line":329},9,[211,331,332],{"class":257},"    \"TABLENAME\"",[211,334,302],{"class":220},[211,336,264],{"class":257},[211,338,326],{"class":220},[211,340,342,345,347,350],{"class":106,"line":341},10,[211,343,344],{"class":257},"    \"PRIMARY_KEY\"",[211,346,302],{"class":220},[211,348,349],{"class":257},"\"hydrant_pk\"",[211,351,326],{"class":220},[211,353,355,358,360,363],{"class":106,"line":354},11,[211,356,357],{"class":257},"    \"GEOMETRY_COLUMN\"",[211,359,302],{"class":220},[211,361,362],{"class":257},"\"geom\"",[211,364,326],{"class":220},[211,366,368,371,373,376],{"class":106,"line":367},12,[211,369,370],{"class":257},"    \"ENCODING\"",[211,372,302],{"class":220},[211,374,375],{"class":257},"\"UTF-8\"",[211,377,326],{"class":220},[211,379,381,384,386,390],{"class":106,"line":380},13,[211,382,383],{"class":257},"    \"OVERWRITE\"",[211,385,302],{"class":220},[211,387,389],{"class":388},"sDLfK","True",[211,391,326],{"class":220},[211,393,395,398,400,402],{"class":106,"line":394},14,[211,396,397],{"class":257},"    \"CREATEINDEX\"",[211,399,302],{"class":220},[211,401,389],{"class":388},[211,403,326],{"class":220},[211,405,407,410,412,414],{"class":106,"line":406},15,[211,408,409],{"class":257},"    \"LOWERCASE_NAMES\"",[211,411,302],{"class":220},[211,413,389],{"class":388},[211,415,326],{"class":220},[211,417,419,422,424,427],{"class":106,"line":418},16,[211,420,421],{"class":257},"    \"DROP_STRING_LENGTH\"",[211,423,302],{"class":220},[211,425,426],{"class":388},"False",[211,428,326],{"class":220},[211,430,432,435,437,439],{"class":106,"line":431},17,[211,433,434],{"class":257},"    \"FORCE_SINGLEPART\"",[211,436,302],{"class":220},[211,438,426],{"class":388},[211,440,326],{"class":220},[211,442,444],{"class":106,"line":443},18,[211,445,446],{"class":220},"})\n",[211,448,450,453,456,459,462,465],{"class":106,"line":449},19,[211,451,452],{"class":388},"print",[211,454,455],{"class":220},"(",[211,457,458],{"class":257},"\"imported\"",[211,460,461],{"class":220},", src.featureCount(), ",[211,463,464],{"class":257},"\"features into water.hydrants\"",[211,466,272],{"class":220},[14,468,469,473,474,477,478,481,482,485,486,489,490,493,494,497],{},[470,471,472],"strong",{},"Breakdown:"," ",[197,475,476],{},"DATABASE"," is the name of a connection saved in QGIS, so no password appears in the script; authentication comes from the connection's stored configuration. The primary key column is created as a serial integer if the source has no suitable key. ",[197,479,480],{},"LOWERCASE_NAMES"," converts field names to lower case, which saves endless double-quoting in SQL — PostgreSQL folds unquoted identifiers to lower case, so a column named ",[197,483,484],{},"HydrantID"," would otherwise have to be written ",[197,487,488],{},"\"HydrantID\""," forever. ",[197,491,492],{},"CREATEINDEX"," builds a GiST spatial index on the geometry. ",[197,495,496],{},"OVERWRITE"," replaces an existing table of the same name, which is right for a reload and dangerous for anything else.",[169,499,501],{"id":500},"control-geometry-type-and-crs","Control geometry type and CRS",[14,503,504,505,508],{},"PostGIS geometry columns are typed: ",[197,506,507],{},"MultiPolygon"," with SRID 25832, for example. A source mixing single and multipart polygons, or in an unexpected CRS, needs adjusting so the table's type is right from the start.",[14,510,511],{},[34,512,515,518,521,524,527,534,539,543,547,551,556,560,563,566],{"viewBox":513,"role":37,"ariaLabel":514,"xmlns":39},"0 0 760 222","Promoting mixed geometries to multi and reprojecting to the database's standard CRS before import so the column type and SRID are right",[41,516,517],{},"Getting the column type right",[45,519,520],{},"A source with both Polygon and MultiPolygon features is promoted to multi before import, so the table's geometry column is MultiPolygon and both kinds fit. A source in EPSG 4326 is reprojected to the database's standard CRS before import, so every table shares one SRID. The column type and SRID are fixed at creation and hard to change later.",[49,522],{"x":51,"y":51,"width":52,"height":523,"fill":54},"222",[73,525,526],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"Fix type and CRS before the table exists",[49,528],{"x":83,"y":529,"width":530,"height":531,"rx":63,"fill":532,"stroke":533,"style":89},"56","340","150","#fdf2e2","#b45309",[73,535,538],{"x":536,"y":537,"style":94,"fill":533,"textAnchor":79},"194","92.78","mixed single\u002Fmulti",[73,540,542],{"x":536,"y":541,"style":99,"fill":71,"textAnchor":79},"120.78","→ native:promotetomulti",[73,544,546],{"x":536,"y":545,"style":99,"fill":71,"textAnchor":79},"148.78","column: MultiPolygon",[73,548,550],{"x":536,"y":549,"style":99,"fill":88,"textAnchor":79},"176.78","both kinds fit",[49,552],{"x":553,"y":529,"width":530,"height":531,"rx":63,"fill":554,"stroke":555,"style":89},"396","#eff3ff","#2563eb",[73,557,559],{"x":558,"y":537,"style":94,"fill":555,"textAnchor":79},"566","foreign CRS",[73,561,562],{"x":558,"y":541,"style":99,"fill":71,"textAnchor":79},"→ native:reprojectlayer",[73,564,565],{"x":558,"y":545,"style":99,"fill":71,"textAnchor":79},"column: SRID 25832",[73,567,568],{"x":558,"y":549,"style":99,"fill":88,"textAnchor":79},"one SRID per database",[202,570,572],{"className":204,"code":571,"language":206,"meta":207,"style":207},"from qgis.core import QgsCoordinateReferenceSystem\n\nDB_CRS = QgsCoordinateReferenceSystem(\"EPSG:25832\")\n\ndef prepare(layer):\n    if layer.crs() != DB_CRS:\n        layer = processing.run(\"native:reprojectlayer\", {\n            \"INPUT\": layer, \"TARGET_CRS\": DB_CRS, \"OUTPUT\": \"memory:\"})[\"OUTPUT\"]\n    layer = processing.run(\"native:promotetomulti\", {\n        \"INPUT\": layer, \"OUTPUT\": \"memory:\"})[\"OUTPUT\"]\n    return layer\n\nparcels = prepare(QgsVectorLayer(\"\u002Fdata\u002Finbox\u002Fparcels.shp\", \"parcels\", \"ogr\"))\nprocessing.run(\"native:importintopostgis\", {\n    \"INPUT\": parcels, \"DATABASE\": \"assets_db\", \"SCHEMA\": \"cadastre\",\n    \"TABLENAME\": \"parcels\", \"PRIMARY_KEY\": \"parcel_pk\", \"GEOMETRY_COLUMN\": \"geom\",\n    \"OVERWRITE\": True, \"CREATEINDEX\": True, \"LOWERCASE_NAMES\": True})\n",[197,573,574,585,589,605,609,621,638,653,686,700,719,727,731,756,764,790,819],{"__ignoreMap":207},[211,575,576,578,580,582],{"class":106,"line":213},[211,577,227],{"class":216},[211,579,230],{"class":220},[211,581,217],{"class":216},[211,583,584],{"class":220}," QgsCoordinateReferenceSystem\n",[211,586,587],{"class":106,"line":224},[211,588,242],{"emptyLinePlaceholder":241},[211,590,591,594,597,600,603],{"class":106,"line":238},[211,592,593],{"class":388},"DB_CRS",[211,595,596],{"class":216}," =",[211,598,599],{"class":220}," QgsCoordinateReferenceSystem(",[211,601,602],{"class":257},"\"EPSG:25832\"",[211,604,272],{"class":220},[211,606,607],{"class":106,"line":245},[211,608,242],{"emptyLinePlaceholder":241},[211,610,611,614,618],{"class":106,"line":275},[211,612,613],{"class":216},"def",[211,615,617],{"class":616},"svObZ"," prepare",[211,619,620],{"class":220},"(layer):\n",[211,622,623,626,629,632,635],{"class":106,"line":287},[211,624,625],{"class":216},"    if",[211,627,628],{"class":220}," layer.crs() ",[211,630,631],{"class":216},"!=",[211,633,634],{"class":388}," DB_CRS",[211,636,637],{"class":220},":\n",[211,639,640,643,645,648,651],{"class":106,"line":296},[211,641,642],{"class":220},"        layer ",[211,644,251],{"class":216},[211,646,647],{"class":220}," processing.run(",[211,649,650],{"class":257},"\"native:reprojectlayer\"",[211,652,284],{"class":220},[211,654,655,658,661,664,666,668,670,673,675,678,681,683],{"class":106,"line":315},[211,656,657],{"class":257},"            \"INPUT\"",[211,659,660],{"class":220},": layer, ",[211,662,663],{"class":257},"\"TARGET_CRS\"",[211,665,302],{"class":220},[211,667,593],{"class":388},[211,669,261],{"class":220},[211,671,672],{"class":257},"\"OUTPUT\"",[211,674,302],{"class":220},[211,676,677],{"class":257},"\"memory:\"",[211,679,680],{"class":220},"})[",[211,682,672],{"class":257},[211,684,685],{"class":220},"]\n",[211,687,688,691,693,695,698],{"class":106,"line":329},[211,689,690],{"class":220},"    layer ",[211,692,251],{"class":216},[211,694,647],{"class":220},[211,696,697],{"class":257},"\"native:promotetomulti\"",[211,699,284],{"class":220},[211,701,702,705,707,709,711,713,715,717],{"class":106,"line":341},[211,703,704],{"class":257},"        \"INPUT\"",[211,706,660],{"class":220},[211,708,672],{"class":257},[211,710,302],{"class":220},[211,712,677],{"class":257},[211,714,680],{"class":220},[211,716,672],{"class":257},[211,718,685],{"class":220},[211,720,721,724],{"class":106,"line":354},[211,722,723],{"class":216},"    return",[211,725,726],{"class":220}," layer\n",[211,728,729],{"class":106,"line":367},[211,730,242],{"emptyLinePlaceholder":241},[211,732,733,736,738,741,744,746,749,751,753],{"class":106,"line":380},[211,734,735],{"class":220},"parcels ",[211,737,251],{"class":216},[211,739,740],{"class":220}," prepare(QgsVectorLayer(",[211,742,743],{"class":257},"\"\u002Fdata\u002Finbox\u002Fparcels.shp\"",[211,745,261],{"class":220},[211,747,748],{"class":257},"\"parcels\"",[211,750,261],{"class":220},[211,752,269],{"class":257},[211,754,755],{"class":220},"))\n",[211,757,758,760,762],{"class":106,"line":394},[211,759,278],{"class":220},[211,761,281],{"class":257},[211,763,284],{"class":220},[211,765,766,768,771,774,776,778,780,783,785,788],{"class":106,"line":406},[211,767,290],{"class":257},[211,769,770],{"class":220},": parcels, ",[211,772,773],{"class":257},"\"DATABASE\"",[211,775,302],{"class":220},[211,777,305],{"class":257},[211,779,261],{"class":220},[211,781,782],{"class":257},"\"SCHEMA\"",[211,784,302],{"class":220},[211,786,787],{"class":257},"\"cadastre\"",[211,789,326],{"class":220},[211,791,792,794,796,798,800,803,805,808,810,813,815,817],{"class":106,"line":418},[211,793,332],{"class":257},[211,795,302],{"class":220},[211,797,748],{"class":257},[211,799,261],{"class":220},[211,801,802],{"class":257},"\"PRIMARY_KEY\"",[211,804,302],{"class":220},[211,806,807],{"class":257},"\"parcel_pk\"",[211,809,261],{"class":220},[211,811,812],{"class":257},"\"GEOMETRY_COLUMN\"",[211,814,302],{"class":220},[211,816,362],{"class":257},[211,818,326],{"class":220},[211,820,821,823,825,827,829,832,834,836,838,841,843,845],{"class":106,"line":431},[211,822,383],{"class":257},[211,824,302],{"class":220},[211,826,389],{"class":388},[211,828,261],{"class":220},[211,830,831],{"class":257},"\"CREATEINDEX\"",[211,833,302],{"class":220},[211,835,389],{"class":388},[211,837,261],{"class":220},[211,839,840],{"class":257},"\"LOWERCASE_NAMES\"",[211,842,302],{"class":220},[211,844,389],{"class":388},[211,846,446],{"class":220},[14,848,849,851,852,856,857,860],{},[470,850,472],{}," Keeping every table in one database in one CRS makes spatial SQL across tables straightforward — joins and intersections need matching SRIDs. Reprojecting during preparation, with a deliberately chosen transformation, is better than discovering mismatches in queries later; see ",[21,853,855],{"href":854},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fbatch-reproject-vector-layers-pyqgis\u002F","batch reprojecting vector layers",". Promoting to multi gives the column a type every feature fits; importing a shapefile that declares ",[197,858,859],{},"Polygon"," but contains multipolygons otherwise fails part-way through.",[169,862,864],{"id":863},"import-with-the-connections-api","Import with the connections API",[14,866,867],{},"For more control — custom options, no Processing dependency, or importing from inside a plugin — the provider connections API exposes the same exporter directly.",[202,869,871],{"className":204,"code":870,"language":206,"meta":207,"style":207},"from qgis.core import QgsProviderRegistry, QgsVectorLayerExporter\n\nmd = QgsProviderRegistry.instance().providerMetadata(\"postgres\")\nconn = md.findConnection(\"assets_db\")\ndest = f'{conn.uri()} table=\"water\".\"valves\" (geom)'\noptions = {\"overwrite\": True, \"lowercaseFieldNames\": True}\nerror, message = QgsVectorLayerExporter.exportLayer(\n    prepare(QgsVectorLayer(\"\u002Fdata\u002Finbox\u002Fvalves.gpkg\", \"valves\", \"ogr\")),\n    dest, \"postgres\", DB_CRS, False, options)\nprint(\"ok\" if error == QgsVectorLayerExporter.NoError else message)\n",[197,872,873,884,888,903,917,942,971,981,1001,1019],{"__ignoreMap":207},[211,874,875,877,879,881],{"class":106,"line":213},[211,876,227],{"class":216},[211,878,230],{"class":220},[211,880,217],{"class":216},[211,882,883],{"class":220}," QgsProviderRegistry, QgsVectorLayerExporter\n",[211,885,886],{"class":106,"line":224},[211,887,242],{"emptyLinePlaceholder":241},[211,889,890,893,895,898,901],{"class":106,"line":238},[211,891,892],{"class":220},"md ",[211,894,251],{"class":216},[211,896,897],{"class":220}," QgsProviderRegistry.instance().providerMetadata(",[211,899,900],{"class":257},"\"postgres\"",[211,902,272],{"class":220},[211,904,905,908,910,913,915],{"class":106,"line":245},[211,906,907],{"class":220},"conn ",[211,909,251],{"class":216},[211,911,912],{"class":220}," md.findConnection(",[211,914,305],{"class":257},[211,916,272],{"class":220},[211,918,919,922,924,927,930,933,936,939],{"class":106,"line":275},[211,920,921],{"class":220},"dest ",[211,923,251],{"class":216},[211,925,926],{"class":216}," f",[211,928,929],{"class":257},"'",[211,931,932],{"class":388},"{",[211,934,935],{"class":220},"conn.uri()",[211,937,938],{"class":388},"}",[211,940,941],{"class":257}," table=\"water\".\"valves\" (geom)'\n",[211,943,944,947,949,952,955,957,959,961,964,966,968],{"class":106,"line":287},[211,945,946],{"class":220},"options ",[211,948,251],{"class":216},[211,950,951],{"class":220}," {",[211,953,954],{"class":257},"\"overwrite\"",[211,956,302],{"class":220},[211,958,389],{"class":388},[211,960,261],{"class":220},[211,962,963],{"class":257},"\"lowercaseFieldNames\"",[211,965,302],{"class":220},[211,967,389],{"class":388},[211,969,970],{"class":220},"}\n",[211,972,973,976,978],{"class":106,"line":296},[211,974,975],{"class":220},"error, message ",[211,977,251],{"class":216},[211,979,980],{"class":220}," QgsVectorLayerExporter.exportLayer(\n",[211,982,983,986,989,991,994,996,998],{"class":106,"line":315},[211,984,985],{"class":220},"    prepare(QgsVectorLayer(",[211,987,988],{"class":257},"\"\u002Fdata\u002Finbox\u002Fvalves.gpkg\"",[211,990,261],{"class":220},[211,992,993],{"class":257},"\"valves\"",[211,995,261],{"class":220},[211,997,269],{"class":257},[211,999,1000],{"class":220},")),\n",[211,1002,1003,1006,1008,1010,1012,1014,1016],{"class":106,"line":329},[211,1004,1005],{"class":220},"    dest, ",[211,1007,900],{"class":257},[211,1009,261],{"class":220},[211,1011,593],{"class":388},[211,1013,261],{"class":220},[211,1015,426],{"class":388},[211,1017,1018],{"class":220},", options)\n",[211,1020,1021,1023,1025,1028,1031,1034,1037,1040,1043],{"class":106,"line":341},[211,1022,452],{"class":388},[211,1024,455],{"class":220},[211,1026,1027],{"class":257},"\"ok\"",[211,1029,1030],{"class":216}," if",[211,1032,1033],{"class":220}," error ",[211,1035,1036],{"class":216},"==",[211,1038,1039],{"class":220}," QgsVectorLayerExporter.NoError ",[211,1041,1042],{"class":216},"else",[211,1044,1045],{"class":220}," message)\n",[14,1047,1048,473,1050,1053,1054,1057,1058,261,1061,261,1064,261,1067,1070,1071,30],{},[470,1049,472],{},[197,1051,1052],{},"QgsVectorLayerExporter.exportLayer"," takes a layer, a destination URI, a provider key, the destination CRS and options, and returns an error code with a message. Building the destination from the connection's own URI plus a ",[197,1055,1056],{},"table=\"schema\".\"name\" (geom)"," clause reuses the stored connection details. Options mirror the Processing parameters: ",[197,1059,1060],{},"overwrite",[197,1062,1063],{},"lowercaseFieldNames",[197,1065,1066],{},"dropStringConstraints",[197,1068,1069],{},"forceSinglePartGeometryType",". The connections API also runs SQL, lists schemas and tables, and drops or renames tables — all covered in ",[21,1072,1074],{"href":1073},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fuse-database-connections-api-pyqgis\u002F","using the database connections API",[169,1076,1078],{"id":1077},"index-analyze-and-comment","Index, analyze and comment",[14,1080,1081],{},"A freshly imported table has a spatial index if requested, but PostgreSQL's planner knows nothing about its contents until statistics are collected. A few statements after import make queries fast and the table self-describing.",[202,1083,1085],{"className":204,"code":1084,"language":206,"meta":207,"style":207},"conn.executeSql(\"\"\"\n    CREATE INDEX IF NOT EXISTS hydrants_geom_idx ON water.hydrants USING gist (geom);\n    CREATE UNIQUE INDEX IF NOT EXISTS hydrants_no_idx ON water.hydrants (hydrant_no);\n    ANALYZE water.hydrants;\n    COMMENT ON TABLE water.hydrants IS 'Hydrants, imported 2026-10-02 from hydrants.gpkg (survey 2026)';\n\"\"\")\n",[197,1086,1087,1095,1100,1105,1110,1115],{"__ignoreMap":207},[211,1088,1089,1092],{"class":106,"line":213},[211,1090,1091],{"class":220},"conn.executeSql(",[211,1093,1094],{"class":257},"\"\"\"\n",[211,1096,1097],{"class":106,"line":224},[211,1098,1099],{"class":257},"    CREATE INDEX IF NOT EXISTS hydrants_geom_idx ON water.hydrants USING gist (geom);\n",[211,1101,1102],{"class":106,"line":238},[211,1103,1104],{"class":257},"    CREATE UNIQUE INDEX IF NOT EXISTS hydrants_no_idx ON water.hydrants (hydrant_no);\n",[211,1106,1107],{"class":106,"line":245},[211,1108,1109],{"class":257},"    ANALYZE water.hydrants;\n",[211,1111,1112],{"class":106,"line":275},[211,1113,1114],{"class":257},"    COMMENT ON TABLE water.hydrants IS 'Hydrants, imported 2026-10-02 from hydrants.gpkg (survey 2026)';\n",[211,1116,1117,1120],{"class":106,"line":287},[211,1118,1119],{"class":257},"\"\"\"",[211,1121,272],{"class":220},[14,1123,1124,1126,1127,1129],{},[470,1125,472],{}," The spatial index is created only if missing, so the statement is safe to repeat. A unique index on the business identifier enforces it in the database, where every client respects it. ",[197,1128,167],{}," collects statistics so the planner chooses index scans for selective queries. A table comment records where the data came from — PostgreSQL tools and QGIS's browser both show it, so provenance travels with the table.",[169,1131,1133],{"id":1132},"import-a-folder-of-files","Import a folder of files",[14,1135,1136],{},"Bulk imports benefit from one function that applies the same naming, preparation and post-processing to every file, and a log of what happened.",[14,1138,1139],{},[34,1140,1143,1146,1149,1152,1159,1162,1167,1172,1176,1180,1184,1188,1192,1196,1200,1203,1207,1210],{"viewBox":1141,"role":37,"ariaLabel":1142,"xmlns":39},"0 0 760 196","Each file in a folder imported as a normalised table in one schema, with failures logged and a summary at the end",[41,1144,1145],{},"Folder to schema",[45,1147,1148],{},"Each file in a folder becomes a table in one schema. File names are normalised into valid table names. Each layer is prepared, imported, indexed and analysed. Failures are logged with their message and do not stop the rest. A summary lists imported tables and row counts.",[49,1150],{"x":51,"y":51,"width":52,"height":1151,"fill":54},"196",[56,1153,1154],{},[59,1155,1157],{"id":1156,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"pgFolderArrow",[68,1158],{"d":70,"fill":71},[73,1160,1161],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"One function, every file, one log",[49,1163],{"x":83,"y":1164,"width":1165,"height":1166,"rx":63,"fill":87,"stroke":88,"style":89},"60","200","120",[73,1168,1171],{"x":1169,"y":1170,"style":94,"fill":78,"textAnchor":79},"124","110.78","folder",[73,1173,1175],{"x":1169,"y":1174,"style":99,"fill":88,"textAnchor":79},"136.78","*.shp, *.gpkg",[106,1177],{"x1":110,"y1":1166,"x2":1178,"y2":1166,"stroke":71,"style":1179},"266","stroke-width:1.8;marker-end:url(#pgFolderArrow)",[49,1181],{"x":1182,"y":1164,"width":1183,"height":1166,"rx":63,"fill":118,"stroke":119,"style":89},"270","220",[73,1185,1187],{"x":75,"y":1186,"style":94,"fill":119,"textAnchor":79},"97.78","per file",[73,1189,1191],{"x":75,"y":1190,"style":99,"fill":88,"textAnchor":79},"123.78","name → prepare",[73,1193,1195],{"x":75,"y":1194,"style":99,"fill":88,"textAnchor":79},"149.78","import → index",[106,1197],{"x1":1198,"y1":1166,"x2":1199,"y2":1166,"stroke":71,"style":1179},"490","532",[49,1201],{"x":1202,"y":1164,"width":1165,"height":1166,"rx":63,"fill":155,"stroke":156,"style":89},"536",[73,1204,1206],{"x":1205,"y":1186,"style":94,"fill":160,"textAnchor":79},"636","schema + log",[73,1208,1209],{"x":1205,"y":1190,"style":99,"fill":88,"textAnchor":79},"tables, counts",[73,1211,1212],{"x":1205,"y":1194,"style":99,"fill":88,"textAnchor":79},"failures",[202,1214,1216],{"className":204,"code":1215,"language":206,"meta":207,"style":207},"import re\nfrom pathlib import Path\n\ndef table_name(stem):\n    return re.sub(r\"[^a-z0-9_]+\", \"_\", stem.lower()).strip(\"_\")[:63]\n\nlog = []\nfor path in sorted(Path(\"\u002Fdata\u002Finbox\u002F2026_delivery\").glob(\"*.shp\")):\n    name = table_name(path.stem)\n    try:\n        lyr = prepare(QgsVectorLayer(str(path), name, \"ogr\"))\n        processing.run(\"native:importintopostgis\", {\n            \"INPUT\": lyr, \"DATABASE\": \"assets_db\", \"SCHEMA\": \"delivery_2026\",\n            \"TABLENAME\": name, \"PRIMARY_KEY\": \"pk\", \"GEOMETRY_COLUMN\": \"geom\",\n            \"OVERWRITE\": True, \"CREATEINDEX\": True, \"LOWERCASE_NAMES\": True})\n        conn.executeSql(f\"ANALYZE delivery_2026.{name};\")\n        log.append((name, lyr.featureCount(), \"ok\"))\n    except Exception as err:\n        log.append((name, 0, f\"FAILED: {err}\"))\nfor row in log:\n    print(*row)\n",[197,1217,1218,1225,1237,1241,1251,1296,1300,1310,1339,1349,1356,1375,1384,1408,1433,1460,1483,1492,1506,1531,1544],{"__ignoreMap":207},[211,1219,1220,1222],{"class":106,"line":213},[211,1221,217],{"class":216},[211,1223,1224],{"class":220}," re\n",[211,1226,1227,1229,1232,1234],{"class":106,"line":224},[211,1228,227],{"class":216},[211,1230,1231],{"class":220}," pathlib ",[211,1233,217],{"class":216},[211,1235,1236],{"class":220}," Path\n",[211,1238,1239],{"class":106,"line":238},[211,1240,242],{"emptyLinePlaceholder":241},[211,1242,1243,1245,1248],{"class":106,"line":245},[211,1244,613],{"class":216},[211,1246,1247],{"class":616}," table_name",[211,1249,1250],{"class":220},"(stem):\n",[211,1252,1253,1255,1258,1261,1264,1267,1270,1273,1276,1278,1280,1283,1286,1288,1291,1294],{"class":106,"line":275},[211,1254,723],{"class":216},[211,1256,1257],{"class":220}," re.sub(",[211,1259,1260],{"class":216},"r",[211,1262,1263],{"class":257},"\"",[211,1265,1266],{"class":388},"[",[211,1268,1269],{"class":216},"^",[211,1271,1272],{"class":388},"a-z0-9_]",[211,1274,1275],{"class":216},"+",[211,1277,1263],{"class":257},[211,1279,261],{"class":220},[211,1281,1282],{"class":257},"\"_\"",[211,1284,1285],{"class":220},", stem.lower()).strip(",[211,1287,1282],{"class":257},[211,1289,1290],{"class":220},")[:",[211,1292,1293],{"class":388},"63",[211,1295,685],{"class":220},[211,1297,1298],{"class":106,"line":287},[211,1299,242],{"emptyLinePlaceholder":241},[211,1301,1302,1305,1307],{"class":106,"line":296},[211,1303,1304],{"class":220},"log ",[211,1306,251],{"class":216},[211,1308,1309],{"class":220}," []\n",[211,1311,1312,1315,1318,1321,1324,1327,1330,1333,1336],{"class":106,"line":315},[211,1313,1314],{"class":216},"for",[211,1316,1317],{"class":220}," path ",[211,1319,1320],{"class":216},"in",[211,1322,1323],{"class":388}," sorted",[211,1325,1326],{"class":220},"(Path(",[211,1328,1329],{"class":257},"\"\u002Fdata\u002Finbox\u002F2026_delivery\"",[211,1331,1332],{"class":220},").glob(",[211,1334,1335],{"class":257},"\"*.shp\"",[211,1337,1338],{"class":220},")):\n",[211,1340,1341,1344,1346],{"class":106,"line":329},[211,1342,1343],{"class":220},"    name ",[211,1345,251],{"class":216},[211,1347,1348],{"class":220}," table_name(path.stem)\n",[211,1350,1351,1354],{"class":106,"line":341},[211,1352,1353],{"class":216},"    try",[211,1355,637],{"class":220},[211,1357,1358,1361,1363,1365,1368,1371,1373],{"class":106,"line":354},[211,1359,1360],{"class":220},"        lyr ",[211,1362,251],{"class":216},[211,1364,740],{"class":220},[211,1366,1367],{"class":388},"str",[211,1369,1370],{"class":220},"(path), name, ",[211,1372,269],{"class":257},[211,1374,755],{"class":220},[211,1376,1377,1380,1382],{"class":106,"line":367},[211,1378,1379],{"class":220},"        processing.run(",[211,1381,281],{"class":257},[211,1383,284],{"class":220},[211,1385,1386,1388,1391,1393,1395,1397,1399,1401,1403,1406],{"class":106,"line":380},[211,1387,657],{"class":257},[211,1389,1390],{"class":220},": lyr, ",[211,1392,773],{"class":257},[211,1394,302],{"class":220},[211,1396,305],{"class":257},[211,1398,261],{"class":220},[211,1400,782],{"class":257},[211,1402,302],{"class":220},[211,1404,1405],{"class":257},"\"delivery_2026\"",[211,1407,326],{"class":220},[211,1409,1410,1413,1416,1418,1420,1423,1425,1427,1429,1431],{"class":106,"line":394},[211,1411,1412],{"class":257},"            \"TABLENAME\"",[211,1414,1415],{"class":220},": name, ",[211,1417,802],{"class":257},[211,1419,302],{"class":220},[211,1421,1422],{"class":257},"\"pk\"",[211,1424,261],{"class":220},[211,1426,812],{"class":257},[211,1428,302],{"class":220},[211,1430,362],{"class":257},[211,1432,326],{"class":220},[211,1434,1435,1438,1440,1442,1444,1446,1448,1450,1452,1454,1456,1458],{"class":106,"line":406},[211,1436,1437],{"class":257},"            \"OVERWRITE\"",[211,1439,302],{"class":220},[211,1441,389],{"class":388},[211,1443,261],{"class":220},[211,1445,831],{"class":257},[211,1447,302],{"class":220},[211,1449,389],{"class":388},[211,1451,261],{"class":220},[211,1453,840],{"class":257},[211,1455,302],{"class":220},[211,1457,389],{"class":388},[211,1459,446],{"class":220},[211,1461,1462,1465,1468,1471,1473,1476,1478,1481],{"class":106,"line":418},[211,1463,1464],{"class":220},"        conn.executeSql(",[211,1466,1467],{"class":216},"f",[211,1469,1470],{"class":257},"\"ANALYZE delivery_2026.",[211,1472,932],{"class":388},[211,1474,1475],{"class":220},"name",[211,1477,938],{"class":388},[211,1479,1480],{"class":257},";\"",[211,1482,272],{"class":220},[211,1484,1485,1488,1490],{"class":106,"line":431},[211,1486,1487],{"class":220},"        log.append((name, lyr.featureCount(), ",[211,1489,1027],{"class":257},[211,1491,755],{"class":220},[211,1493,1494,1497,1500,1503],{"class":106,"line":443},[211,1495,1496],{"class":216},"    except",[211,1498,1499],{"class":388}," Exception",[211,1501,1502],{"class":216}," as",[211,1504,1505],{"class":220}," err:\n",[211,1507,1508,1511,1513,1515,1517,1520,1522,1525,1527,1529],{"class":106,"line":449},[211,1509,1510],{"class":220},"        log.append((name, ",[211,1512,51],{"class":388},[211,1514,261],{"class":220},[211,1516,1467],{"class":216},[211,1518,1519],{"class":257},"\"FAILED: ",[211,1521,932],{"class":388},[211,1523,1524],{"class":220},"err",[211,1526,938],{"class":388},[211,1528,1263],{"class":257},[211,1530,755],{"class":220},[211,1532,1534,1536,1539,1541],{"class":106,"line":1533},20,[211,1535,1314],{"class":216},[211,1537,1538],{"class":220}," row ",[211,1540,1320],{"class":216},[211,1542,1543],{"class":220}," log:\n",[211,1545,1547,1550,1552,1555],{"class":106,"line":1546},21,[211,1548,1549],{"class":388},"    print",[211,1551,455],{"class":220},[211,1553,1554],{"class":216},"*",[211,1556,1557],{"class":220},"row)\n",[14,1559,1560,1562],{},[470,1561,472],{}," Normalising file names into lower-case, underscore-separated identifiers of at most 63 characters — PostgreSQL's limit — avoids quoting and truncation problems. Catching exceptions per file means one bad shapefile does not abort the whole delivery; the log names it and the reason. Importing into a dedicated schema per delivery keeps raw imports separate from curated tables, which can then be built with SQL from them.",[169,1564,1566],{"id":1565},"grant-access-and-register-the-table","Grant access and register the table",[14,1568,1569],{},"A table nobody else can read is not yet shared. Most databases separate the role that loads data from the roles that read and edit it, so an import script should finish by granting the right privileges — and, where an organisation keeps a catalogue of datasets, recording the new table there.",[202,1571,1573],{"className":204,"code":1572,"language":206,"meta":207,"style":207},"conn.executeSql(\"\"\"\n    GRANT USAGE ON SCHEMA water TO gis_readers, gis_editors;\n    GRANT SELECT ON water.hydrants TO gis_readers;\n    GRANT SELECT, INSERT, UPDATE, DELETE ON water.hydrants TO gis_editors;\n    GRANT USAGE ON SEQUENCE water.hydrants_hydrant_pk_seq TO gis_editors;\n\"\"\")\n",[197,1574,1575,1581,1586,1591,1596,1601],{"__ignoreMap":207},[211,1576,1577,1579],{"class":106,"line":213},[211,1578,1091],{"class":220},[211,1580,1094],{"class":257},[211,1582,1583],{"class":106,"line":224},[211,1584,1585],{"class":257},"    GRANT USAGE ON SCHEMA water TO gis_readers, gis_editors;\n",[211,1587,1588],{"class":106,"line":238},[211,1589,1590],{"class":257},"    GRANT SELECT ON water.hydrants TO gis_readers;\n",[211,1592,1593],{"class":106,"line":245},[211,1594,1595],{"class":257},"    GRANT SELECT, INSERT, UPDATE, DELETE ON water.hydrants TO gis_editors;\n",[211,1597,1598],{"class":106,"line":275},[211,1599,1600],{"class":257},"    GRANT USAGE ON SEQUENCE water.hydrants_hydrant_pk_seq TO gis_editors;\n",[211,1602,1603,1605],{"class":106,"line":287},[211,1604,1119],{"class":257},[211,1606,272],{"class":220},[14,1608,1609,1611,1612,1615,1616,1619,1620,1623,1624,1627],{},[470,1610,472],{}," Readers need ",[197,1613,1614],{},"USAGE"," on the schema and ",[197,1617,1618],{},"SELECT"," on the table; editors also need write privileges and use of the primary key's sequence, without which inserts from QGIS fail with a permission error that names the sequence rather than the table. Granting to group roles rather than individual users keeps permissions manageable as people join and leave. The sequence name follows PostgreSQL's convention ",[197,1621,1622],{},"table_column_seq"," for serial columns; check it with ",[197,1625,1626],{},"\\d water.hydrants"," in psql if your import named the key differently.",[169,1629,1631],{"id":1630},"verify-the-import","Verify the import",[14,1633,1634],{},"Counts, extents and a sample of values should match between source and table.",[202,1636,1638],{"className":204,"code":1637,"language":206,"meta":207,"style":207},"from qgis.core import QgsDataSourceUri\n\ncheck_uri = QgsDataSourceUri(conn.uri())\ncheck_uri.setDataSource(\"water\", \"hydrants\", \"geom\", \"\", \"hydrant_pk\")\ntable = QgsVectorLayer(check_uri.uri(False), \"check\", \"postgres\")\nassert table.isValid(), \"table not readable\"\nassert table.featureCount() == src.featureCount(), \"row count differs\"\nprint(table.crs().authid(), table.wkbType(), table.extent().toString(0))\n",[197,1639,1640,1651,1655,1665,1691,1715,1726,1741],{"__ignoreMap":207},[211,1641,1642,1644,1646,1648],{"class":106,"line":213},[211,1643,227],{"class":216},[211,1645,230],{"class":220},[211,1647,217],{"class":216},[211,1649,1650],{"class":220}," QgsDataSourceUri\n",[211,1652,1653],{"class":106,"line":224},[211,1654,242],{"emptyLinePlaceholder":241},[211,1656,1657,1660,1662],{"class":106,"line":238},[211,1658,1659],{"class":220},"check_uri ",[211,1661,251],{"class":216},[211,1663,1664],{"class":220}," QgsDataSourceUri(conn.uri())\n",[211,1666,1667,1670,1672,1674,1676,1678,1680,1682,1685,1687,1689],{"class":106,"line":245},[211,1668,1669],{"class":220},"check_uri.setDataSource(",[211,1671,323],{"class":257},[211,1673,261],{"class":220},[211,1675,264],{"class":257},[211,1677,261],{"class":220},[211,1679,362],{"class":257},[211,1681,261],{"class":220},[211,1683,1684],{"class":257},"\"\"",[211,1686,261],{"class":220},[211,1688,349],{"class":257},[211,1690,272],{"class":220},[211,1692,1693,1696,1698,1701,1703,1706,1709,1711,1713],{"class":106,"line":275},[211,1694,1695],{"class":220},"table ",[211,1697,251],{"class":216},[211,1699,1700],{"class":220}," QgsVectorLayer(check_uri.uri(",[211,1702,426],{"class":388},[211,1704,1705],{"class":220},"), ",[211,1707,1708],{"class":257},"\"check\"",[211,1710,261],{"class":220},[211,1712,900],{"class":257},[211,1714,272],{"class":220},[211,1716,1717,1720,1723],{"class":106,"line":287},[211,1718,1719],{"class":216},"assert",[211,1721,1722],{"class":220}," table.isValid(), ",[211,1724,1725],{"class":257},"\"table not readable\"\n",[211,1727,1728,1730,1733,1735,1738],{"class":106,"line":296},[211,1729,1719],{"class":216},[211,1731,1732],{"class":220}," table.featureCount() ",[211,1734,1036],{"class":216},[211,1736,1737],{"class":220}," src.featureCount(), ",[211,1739,1740],{"class":257},"\"row count differs\"\n",[211,1742,1743,1745,1748,1750],{"class":106,"line":315},[211,1744,452],{"class":388},[211,1746,1747],{"class":220},"(table.crs().authid(), table.wkbType(), table.extent().toString(",[211,1749,51],{"class":388},[211,1751,755],{"class":220},[14,1753,1754,1756],{},[470,1755,472],{}," Loading the new table as a layer through the same connection confirms that QGIS can read it with the intended key and geometry column. Matching feature counts catch features rejected for geometry type. Comparing the extent with the source's (after any reprojection) catches CRS mistakes. A spot check of a few attribute values by key catches encoding problems with non-ASCII text, which are otherwise discovered by users months later.",[169,1758,1760],{"id":1759},"qgis-version-compatibility","QGIS version compatibility",[14,1762,1763,1765,1766,1769,1770,1773,1774,1777,1778,1781,1782,30],{},[197,1764,199],{}," replaced ",[197,1767,1768],{},"qgis:importintopostgis"," in QGIS 3.24 and is available on 3.34 LTR, 3.40 LTR and QGIS 4 with the parameters shown. The connections API, ",[197,1771,1772],{},"QgsVectorLayerExporter"," and ",[197,1775,1776],{},"executeSql"," are available on all these releases. On QGIS 4, ",[197,1779,1780],{},"QgsVectorLayerExporter.NoError"," is ",[197,1783,1784],{},"Qgis.VectorExportResult.Success",[169,1786,1788],{"id":1787},"troubleshooting","Troubleshooting",[174,1790,1791,1797,1803,1812],{},[177,1792,1793,1796],{},[470,1794,1795],{},"The import fails partway."," Mixed single and multipart geometry; promote to multi first.",[177,1798,1799,1802],{},[470,1800,1801],{},"Columns have quoted mixed-case names."," Lower-case names during import.",[177,1804,1805,1808,1809,1811],{},[470,1806,1807],{},"Spatial queries ignore the index."," Run ",[197,1810,167],{},", and make sure queries use the same SRID as the column.",[177,1813,1814,1817,1818,1821,1822,30],{},[470,1815,1816],{},"Accented characters are garbled."," The source encoding was wrong; set ",[197,1819,1820],{},"ENCODING"," or fix the shapefile's ",[197,1823,1824],{},".cpg",[169,1826,1828],{"id":1827},"conclusion","Conclusion",[14,1830,1831,1832,1834],{},"Prepare layers — one CRS for the database, multi geometry types — import with ",[197,1833,199],{}," or the exporter through a stored connection, lower-case names, create spatial and key indexes, analyze and comment tables, import folders with normalised names and a log, and verify counts, CRS and extents.",[169,1836,1838],{"id":1837},"frequently-asked-questions","Frequently Asked Questions",[14,1840,1841,1844,1845,1848],{},[470,1842,1843],{},"Is ogr2ogr faster for huge imports?","\nOften yes, especially with ",[197,1846,1847],{},"-gt"," for large transactions. The PyQGIS route is more convenient when data is already loaded or prepared in QGIS.",[14,1850,1851,1854,1855,30],{},[470,1852,1853],{},"Can I import into an existing table?","\nYes, without overwrite — but columns must match. For routine appends see ",[21,1856,1857],{"href":28},"appending features",[14,1859,1860,1863],{},[470,1861,1862],{},"How do I import a CSV without geometry?","\nImport it the same way; a layer without geometry becomes a plain table.",[14,1865,1866,1869],{},[470,1867,1868],{},"Should the primary key be the business id?","\nKeep a surrogate serial key for the database and a separate unique business id for people.",[169,1871,1873],{"id":1872},"related","Related",[174,1875,1876,1881,1886,1891,1897],{},[177,1877,1878,1880],{},[21,1879,24],{"href":23}," — the guide this recipe belongs to",[177,1882,1883],{},[21,1884,1885],{"href":1073},"Use the Database Connections API in PyQGIS",[177,1887,1888],{},[21,1889,1890],{"href":28},"Append Features to a PostGIS Table in PyQGIS",[177,1892,1893],{},[21,1894,1896],{"href":1895},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fexecute-sql-on-postgis-pyqgis\u002F","Execute SQL on PostGIS in PyQGIS",[177,1898,1899],{},[21,1900,1901],{"href":185},"Connect to a PostGIS Database in PyQGIS",[1903,1904,1905],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":207,"searchDepth":224,"depth":224,"links":1907},[1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920],{"id":171,"depth":224,"text":172},{"id":192,"depth":224,"text":193},{"id":500,"depth":224,"text":501},{"id":863,"depth":224,"text":864},{"id":1077,"depth":224,"text":1078},{"id":1132,"depth":224,"text":1133},{"id":1565,"depth":224,"text":1566},{"id":1630,"depth":224,"text":1631},{"id":1759,"depth":224,"text":1760},{"id":1787,"depth":224,"text":1788},{"id":1827,"depth":224,"text":1828},{"id":1837,"depth":224,"text":1838},{"id":1872,"depth":224,"text":1873},"Load vector layers into PostgreSQL\u002FPostGIS from Python — native:importintopostgis and the connections API's exporter, choosing schema, table name, primary key and geometry column, forcing geometry types, building spatial indexes, and importing whole folders safely.","md",{"slug":1924,"type":1925,"breadcrumb":1926,"datePublished":1927,"dateModified":1927},"import-layer-into-postgis-pyqgis","article","Import a Layer into PostGIS","2026-10-02","\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fimport-layer-into-postgis-pyqgis",{"title":5,"description":1921},"spatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fimport-layer-into-postgis-pyqgis\u002Findex","lRiRE7Tu4N2PuECb-_mXJmaGs3BJuDXct7LMJ1067eU",1790966264214]