[{"data":1,"prerenderedAt":2208},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats":3},{"id":4,"title":5,"body":6,"description":2197,"extension":2198,"meta":2199,"navigation":298,"path":2204,"seo":2205,"stem":2206,"__hash__":2207},"docs\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Findex.md","Layer Data Sources & Formats in PyQGIS",{"type":7,"value":8,"toc":2185},"minimark",[9,13,22,31,200,205,212,239,266,414,433,441,445,451,547,555,567,819,824,828,831,892,907,1097,1115,1123,1127,1134,1206,1226,1333,1344,1348,1351,1435,1447,1531,1536,1540,1546,1549,1635,1845,1874,1878,1881,1930,2029,2032,2036,2074,2078,2084,2098,2107,2117,2127,2131,2181],[10,11,5],"h1",{"id":12},"layer-data-sources-formats-in-pyqgis",[14,15,16,17,21],"p",{},"Every layer in QGIS is two things: a provider that knows how to read a kind of data, and a source string that tells that provider where to find it. Most PyQGIS scripts treat the second part as an opaque path passed to ",[18,19,20],"code",{},"QgsVectorLayer"," and never think about it again — until a CSV arrives with decimal commas, a GeoPackage turns out to hold forty tables, a file server is renamed, or somebody needs a join across a spreadsheet and a database table without copying either.",[14,23,24,25,30],{},"This guide sits inside ",[26,27,29],"a",{"href":28},"\u002Fspatial-data-processing-automation\u002F","Spatial Data Processing & Automation"," and covers the layer between your script and your files. It explains how providers and source strings work, how to read and build them safely, how to handle containers that hold many layers, and how to get data in from the formats that are not really GIS formats at all: spreadsheets, SQL views and photographs.",[14,32,33],{},[34,35,40,44,48,55,72,81,91,96,101,108,116,122,127,131,138,141,143,146,150,159,165,170,174,177,181,183,187,192,196],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 330","img","How a layer reaches its data: a script creates a layer from a provider key and a source string, the provider registry selects a provider, and the provider reads from files, containers, databases, web services or other layers","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Provider key plus source string",[45,46,47],"desc",{},"A script calls QgsVectorLayer or QgsRasterLayer with a source string and a provider key. The provider registry hands the source to the matching provider: ogr and gdal for files and containers such as GeoPackage, delimitedtext for CSV, postgres for databases, wms and WFS for web services, and virtual for SQL views over other layers. Every provider returns the same layer interface, which is why the rest of PyQGIS does not care where data came from.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","330","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"ldsHubArrow","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-weight:bold;font-family:sans-serif","#17211d","middle","Different data, one layer interface",[49,82],{"x":83,"y":84,"width":85,"height":86,"rx":87,"fill":88,"stroke":89,"style":90},"24","130","160","80","10","#fffdf7","#59645f","stroke-width:2",[73,92,95],{"x":93,"y":85,"style":94,"fill":78,"textAnchor":79},"104","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","your script",[73,97,100],{"x":93,"y":98,"style":99,"fill":71,"textAnchor":79},"182","text-anchor:middle;font-size:9.5px;font-family:monospace","source, key",[102,103],"line",{"x1":104,"y1":105,"x2":106,"y2":105,"stroke":71,"style":107},"184","170","226","stroke-width:2;marker-end:url(#ldsHubArrow)",[49,109],{"x":110,"y":111,"width":105,"height":112,"rx":87,"fill":113,"stroke":114,"style":115},"234","122","96","#eef7f4","#0f766e","stroke-width:2.5",[73,117,121],{"x":118,"y":119,"style":120,"fill":114,"textAnchor":79},"319","156","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","provider registry",[73,123,126],{"x":118,"y":124,"style":125,"fill":71,"textAnchor":79},"180","text-anchor:middle;font-size:10px;font-family:sans-serif","picks the reader",[73,128,130],{"x":118,"y":129,"style":125,"fill":71,"textAnchor":79},"198","by provider key",[102,132],{"x1":133,"y1":134,"x2":135,"y2":136,"stroke":71,"style":137},"404","140","470","70","stroke-width:1.6;marker-end:url(#ldsHubArrow)",[102,139],{"x1":133,"y1":119,"x2":135,"y2":140,"stroke":71,"style":137},"124",[102,142],{"x1":133,"y1":105,"x2":135,"y2":105,"stroke":71,"style":137},[102,144],{"x1":133,"y1":104,"x2":135,"y2":145,"stroke":71,"style":137},"216",[102,147],{"x1":133,"y1":148,"x2":135,"y2":149,"stroke":71,"style":137},"200","266",[49,151],{"x":152,"y":153,"width":154,"height":155,"rx":63,"fill":156,"stroke":157,"style":158},"478","48","258","40","#eff3ff","#2563eb","stroke-width:1.8",[73,160,164],{"x":161,"y":162,"style":163,"fill":71,"textAnchor":79},"607","73","text-anchor:middle;font-size:10.5px;font-family:sans-serif","ogr · gdal — files and containers",[49,166],{"x":152,"y":167,"width":154,"height":155,"rx":63,"fill":168,"stroke":169,"style":158},"102","#fdf2e2","#b45309",[73,171,173],{"x":161,"y":172,"style":163,"fill":71,"textAnchor":79},"127","delimitedtext — CSV and text",[49,175],{"x":152,"y":176,"width":154,"height":155,"rx":63,"fill":156,"stroke":157,"style":158},"150",[73,178,180],{"x":161,"y":179,"style":163,"fill":71,"textAnchor":79},"175","postgres · mssql — databases",[49,182],{"x":152,"y":129,"width":154,"height":155,"rx":63,"fill":113,"stroke":114,"style":158},[73,184,186],{"x":161,"y":185,"style":163,"fill":71,"textAnchor":79},"223","wms · WFS · OAPIF — web services",[49,188],{"x":152,"y":189,"width":154,"height":155,"rx":63,"fill":190,"stroke":191,"style":158},"246","#e8efe6","#15803d",[73,193,195],{"x":161,"y":194,"style":163,"fill":71,"textAnchor":79},"271","virtual — SQL over other layers",[73,197,199],{"x":148,"y":198,"style":163,"fill":89,"textAnchor":79},"300","everything downstream sees a QgsVectorLayer",[201,202,204],"h2",{"id":203},"providers-and-source-strings","Providers and source strings",[14,206,207,208,211],{},"A layer object does very little reading of its own. When you write ",[18,209,210],{},"QgsVectorLayer(\"\u002Fdata\u002Froads.gpkg|layername=roads\", \"roads\", \"ogr\")",", the third argument is a provider key, and the provider registry uses it to create a data provider — the object that actually opens files, runs queries, and iterates features. The layer then delegates to it. Styling, labelling, expressions, Processing and layouts all talk to the layer, which is why they work identically whether the features came from a shapefile, a PostGIS table or a WFS.",[14,213,214,215,218,219,222,223,226,227,230,231,234,235,238],{},"The source string is the provider's private language. The ",[18,216,217],{},"ogr"," provider expects a path with optional ",[18,220,221],{},"|layername="," and ",[18,224,225],{},"|subset="," suffixes. The ",[18,228,229],{},"delimitedtext"," provider expects a ",[18,232,233],{},"file:\u002F\u002F"," URL with query parameters. Database providers expect ",[18,236,237],{},"key='value'"," pairs; web providers expect URL-encoded parameter lists. None of these formats is documented as a stable public contract, and all of them have escaping rules that break naive string handling.",[14,240,241,242,245,246,249,250,254,255,258,259,222,262,265],{},"Two rules follow. First, always check ",[18,243,244],{},"isValid()"," after creating a layer, because a provider that cannot open its source still produces a layer object — it just has no data. Second, never assemble or pick apart a source string with ",[18,247,248],{},"split"," and f-strings when the registry can do it for you. ",[26,251,253],{"href":252},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fdecode-and-build-data-source-uris-pyqgis\u002F","Decoding and building data source URIs"," covers ",[18,256,257],{},"decodeUri",", ",[18,260,261],{},"encodeUri",[18,263,264],{},"QgsDataSourceUri",", which between them handle every provider family.",[267,268,273],"pre",{"className":269,"code":270,"language":271,"meta":272,"style":272},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsProviderRegistry\n\nregistry = QgsProviderRegistry.instance()\nprint(sorted(registry.providerList()))\n\nfor layer in QgsProject.instance().mapLayers().values():\n    print(f\"{layer.name():\u003C28} {layer.providerType():\u003C14} valid={layer.isValid()}\")\n    print(\"    \", registry.decodeUri(layer.providerType(), layer.source()))\n","python","",[18,274,275,293,300,312,328,333,348,401],{"__ignoreMap":272},[276,277,279,283,287,290],"span",{"class":102,"line":278},1,[276,280,282],{"class":281},"snl16","from",[276,284,286],{"class":285},"s95oV"," qgis.core ",[276,288,289],{"class":281},"import",[276,291,292],{"class":285}," QgsProject, QgsProviderRegistry\n",[276,294,296],{"class":102,"line":295},2,[276,297,299],{"emptyLinePlaceholder":298},true,"\n",[276,301,303,306,309],{"class":102,"line":302},3,[276,304,305],{"class":285},"registry ",[276,307,308],{"class":281},"=",[276,310,311],{"class":285}," QgsProviderRegistry.instance()\n",[276,313,315,319,322,325],{"class":102,"line":314},4,[276,316,318],{"class":317},"sDLfK","print",[276,320,321],{"class":285},"(",[276,323,324],{"class":317},"sorted",[276,326,327],{"class":285},"(registry.providerList()))\n",[276,329,331],{"class":102,"line":330},5,[276,332,299],{"emptyLinePlaceholder":298},[276,334,336,339,342,345],{"class":102,"line":335},6,[276,337,338],{"class":281},"for",[276,340,341],{"class":285}," layer ",[276,343,344],{"class":281},"in",[276,346,347],{"class":285}," QgsProject.instance().mapLayers().values():\n",[276,349,351,354,356,359,363,366,369,372,375,378,381,384,386,389,391,394,396,398],{"class":102,"line":350},7,[276,352,353],{"class":317},"    print",[276,355,321],{"class":285},[276,357,358],{"class":281},"f",[276,360,362],{"class":361},"sU2Wk","\"",[276,364,365],{"class":317},"{",[276,367,368],{"class":285},"layer.name()",[276,370,371],{"class":281},":\u003C28",[276,373,374],{"class":317},"}",[276,376,377],{"class":317}," {",[276,379,380],{"class":285},"layer.providerType()",[276,382,383],{"class":281},":\u003C14",[276,385,374],{"class":317},[276,387,388],{"class":361}," valid=",[276,390,365],{"class":317},[276,392,393],{"class":285},"layer.isValid()",[276,395,374],{"class":317},[276,397,362],{"class":361},[276,399,400],{"class":285},")\n",[276,402,404,406,408,411],{"class":102,"line":403},8,[276,405,353],{"class":317},[276,407,321],{"class":285},[276,409,410],{"class":361},"\"    \"",[276,412,413],{"class":285},", registry.decodeUri(layer.providerType(), layer.source()))\n",[14,415,416,420,421,424,425,428,429,432],{},[417,418,419],"strong",{},"Breakdown:"," ",[18,422,423],{},"providerList()"," shows which providers your installation actually has — a minimal container image may lack ",[18,426,427],{},"wms"," or ",[18,430,431],{},"mssql",", which explains an invalid layer faster than any error message. Printing the provider type beside each layer is the fastest way to understand an unfamiliar project; decoding the source shows you the parts a script is likely to need, such as the file path behind each layer.",[14,434,435,436,440],{},"When a layer refuses to open, the provider's error and the underlying GDAL message are where the reason lives; ",[26,437,439],{"href":438},"\u002Fpyqgis-fundamentals-environment-setup\u002Fdebugging-pyqgis-scripts\u002Fdebug-invalid-layer-loading-pyqgis\u002F","debugging invalid layer loading"," walks through reading them.",[201,442,444],{"id":443},"text-files-that-describe-geometry","Text files that describe geometry",[14,446,447,448,450],{},"The most common input that is not a GIS format is a spreadsheet export. The ",[18,449,229],{}," provider reads CSV and other delimited files in place, turning two coordinate columns or one WKT column into geometry. It is fast, it keeps the file as the single source of truth, and it has two properties that surprise people: it is read-only, and it never rejects a row. A row with a blank coordinate or an unparseable number becomes a feature with null geometry — present in the attribute table, invisible on the map, and uncounted unless you look.",[14,452,453],{},[34,454,457,460,463,466,469,474,480,484,487,493,498,502,505,508,511,514,518,521,524,527,531,534,537,541,544],{"viewBox":455,"role":37,"ariaLabel":456,"xmlns":39},"0 0 760 270","Three decisions for a text-based source: how the file is parsed, where the geometry comes from, and what coordinate system the numbers are in, with the silent failure each wrong answer produces",[41,458,459],{},"What goes wrong when the URI is vague",[45,461,462],{},"Three columns. Parsing: a wrong delimiter or decimal separator produces a single column or null geometries. Geometry: an auto-detected coordinate column or guessed WKT type produces the wrong layer type. CRS: a missing crs parameter produces points in the wrong place. Each failure still yields a valid layer, which is why they go unnoticed.",[49,464],{"x":51,"y":51,"width":52,"height":465,"fill":54},"270",[73,467,468],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"Every wrong answer still gives a valid layer",[49,470],{"x":83,"y":471,"width":472,"height":473,"rx":87,"fill":88,"stroke":114,"style":115},"50","222","196",[73,475,479],{"x":476,"y":477,"style":478,"fill":114,"textAnchor":79},"135","76","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","parsing",[73,481,483],{"x":476,"y":93,"style":482,"fill":71,"textAnchor":79},"text-anchor:middle;font-size:10px;font-family:monospace","delimiter, decimalPoint",[73,485,486],{"x":476,"y":140,"style":482,"fill":71,"textAnchor":79},"detectTypes, encoding",[102,488],{"x1":489,"y1":490,"x2":145,"y2":490,"stroke":491,"style":492},"54","146","#d9d3c4","stroke-width:1.5",[73,494,497],{"x":476,"y":495,"style":125,"fill":496,"textAnchor":79},"172","#b91c1c","wrong: numbers as text,",[73,499,501],{"x":476,"y":500,"style":125,"fill":496,"textAnchor":79},"190","geometry silently null",[49,503],{"x":504,"y":471,"width":472,"height":473,"rx":87,"fill":88,"stroke":157,"style":115},"269",[73,506,507],{"x":75,"y":477,"style":478,"fill":157,"textAnchor":79},"geometry",[73,509,510],{"x":75,"y":93,"style":482,"fill":71,"textAnchor":79},"xField, yField",[73,512,513],{"x":75,"y":140,"style":482,"fill":71,"textAnchor":79},"or wktField + geomType",[102,515],{"x1":516,"y1":490,"x2":517,"y2":490,"stroke":491,"style":492},"299","461",[73,519,520],{"x":75,"y":495,"style":125,"fill":496,"textAnchor":79},"wrong: a guessed column",[73,522,523],{"x":75,"y":500,"style":125,"fill":496,"textAnchor":79},"or the wrong layer type",[49,525],{"x":526,"y":471,"width":472,"height":473,"rx":87,"fill":88,"stroke":169,"style":115},"514",[73,528,530],{"x":529,"y":477,"style":478,"fill":169,"textAnchor":79},"625","meaning",[73,532,533],{"x":529,"y":93,"style":482,"fill":71,"textAnchor":79},"crs=EPSG:…",[73,535,536],{"x":529,"y":140,"style":125,"fill":71,"textAnchor":79},"what the numbers are",[102,538],{"x1":539,"y1":490,"x2":540,"y2":490,"stroke":491,"style":492},"544","706",[73,542,543],{"x":529,"y":495,"style":125,"fill":496,"textAnchor":79},"wrong: points in the",[73,545,546],{"x":529,"y":500,"style":125,"fill":496,"textAnchor":79},"wrong hemisphere",[14,548,549,550,554],{},"The defence is to be explicit about everything the provider would otherwise guess — delimiter, decimal separator, encoding, coordinate fields, geometry type and CRS — and to count null geometries immediately after loading. ",[26,551,553],{"href":552},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fload-csv-as-point-layer-pyqgis\u002F","Loading a CSV as a point layer"," builds the URI from a dictionary, handles European number formats and WKT columns, and copies the result into GeoPackage once it looks right.",[14,556,557,558,561,562,566],{},"Photographs are a text source of a different kind: the coordinates live in each file's EXIF header rather than in a column. ",[18,559,560],{},"native:importphotos"," reads a whole folder of them into a point layer with direction and timestamp fields, and ",[26,563,565],{"href":564},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fimport-geotagged-photos-as-points-pyqgis\u002F","importing geotagged photos"," adds the reconciliation step that catches photos without a fix, plus rotated markers and photo map tips.",[267,568,570],{"className":269,"code":569,"language":271,"meta":272,"style":272},"from pathlib import Path\nfrom urllib.parse import urlencode\nfrom qgis.core import QgsVectorLayer, QgsFeatureRequest\n\nparams = {\"type\": \"csv\", \"delimiter\": \";\", \"decimalPoint\": \",\",\n          \"detectTypes\": \"yes\", \"xField\": \"x\", \"yField\": \"y\",\n          \"crs\": \"EPSG:25832\"}\nuri = f\"{Path('\u002Fdata\u002Finbox\u002Fmesspunkte.csv').as_uri()}?{urlencode(params)}\"\npoints = QgsVectorLayer(uri, \"messpunkte\", \"delimitedtext\")\n\nempty = sum(1 for _ in points.getFeatures(\n    QgsFeatureRequest().setFilterExpression(\"$geometry IS NULL\")))\nprint(points.featureCount(), \"rows,\", empty, \"without geometry\")\n",[18,571,572,584,596,607,611,652,684,697,735,756,761,788,800],{"__ignoreMap":272},[276,573,574,576,579,581],{"class":102,"line":278},[276,575,282],{"class":281},[276,577,578],{"class":285}," pathlib ",[276,580,289],{"class":281},[276,582,583],{"class":285}," Path\n",[276,585,586,588,591,593],{"class":102,"line":295},[276,587,282],{"class":281},[276,589,590],{"class":285}," urllib.parse ",[276,592,289],{"class":281},[276,594,595],{"class":285}," urlencode\n",[276,597,598,600,602,604],{"class":102,"line":302},[276,599,282],{"class":281},[276,601,286],{"class":285},[276,603,289],{"class":281},[276,605,606],{"class":285}," QgsVectorLayer, QgsFeatureRequest\n",[276,608,609],{"class":102,"line":314},[276,610,299],{"emptyLinePlaceholder":298},[276,612,613,616,618,620,623,626,629,631,634,636,639,641,644,646,649],{"class":102,"line":330},[276,614,615],{"class":285},"params ",[276,617,308],{"class":281},[276,619,377],{"class":285},[276,621,622],{"class":361},"\"type\"",[276,624,625],{"class":285},": ",[276,627,628],{"class":361},"\"csv\"",[276,630,258],{"class":285},[276,632,633],{"class":361},"\"delimiter\"",[276,635,625],{"class":285},[276,637,638],{"class":361},"\";\"",[276,640,258],{"class":285},[276,642,643],{"class":361},"\"decimalPoint\"",[276,645,625],{"class":285},[276,647,648],{"class":361},"\",\"",[276,650,651],{"class":285},",\n",[276,653,654,657,659,662,664,667,669,672,674,677,679,682],{"class":102,"line":335},[276,655,656],{"class":361},"          \"detectTypes\"",[276,658,625],{"class":285},[276,660,661],{"class":361},"\"yes\"",[276,663,258],{"class":285},[276,665,666],{"class":361},"\"xField\"",[276,668,625],{"class":285},[276,670,671],{"class":361},"\"x\"",[276,673,258],{"class":285},[276,675,676],{"class":361},"\"yField\"",[276,678,625],{"class":285},[276,680,681],{"class":361},"\"y\"",[276,683,651],{"class":285},[276,685,686,689,691,694],{"class":102,"line":350},[276,687,688],{"class":361},"          \"crs\"",[276,690,625],{"class":285},[276,692,693],{"class":361},"\"EPSG:25832\"",[276,695,696],{"class":285},"}\n",[276,698,699,702,704,707,709,711,714,717,720,722,725,727,730,732],{"class":102,"line":403},[276,700,701],{"class":285},"uri ",[276,703,308],{"class":281},[276,705,706],{"class":281}," f",[276,708,362],{"class":361},[276,710,365],{"class":317},[276,712,713],{"class":285},"Path(",[276,715,716],{"class":361},"'\u002Fdata\u002Finbox\u002Fmesspunkte.csv'",[276,718,719],{"class":285},").as_uri()",[276,721,374],{"class":317},[276,723,724],{"class":361},"?",[276,726,365],{"class":317},[276,728,729],{"class":285},"urlencode(params)",[276,731,374],{"class":317},[276,733,734],{"class":361},"\"\n",[276,736,738,741,743,746,749,751,754],{"class":102,"line":737},9,[276,739,740],{"class":285},"points ",[276,742,308],{"class":281},[276,744,745],{"class":285}," QgsVectorLayer(uri, ",[276,747,748],{"class":361},"\"messpunkte\"",[276,750,258],{"class":285},[276,752,753],{"class":361},"\"delimitedtext\"",[276,755,400],{"class":285},[276,757,759],{"class":102,"line":758},10,[276,760,299],{"emptyLinePlaceholder":298},[276,762,764,767,769,772,774,777,780,783,785],{"class":102,"line":763},11,[276,765,766],{"class":285},"empty ",[276,768,308],{"class":281},[276,770,771],{"class":317}," sum",[276,773,321],{"class":285},[276,775,776],{"class":317},"1",[276,778,779],{"class":281}," for",[276,781,782],{"class":285}," _ ",[276,784,344],{"class":281},[276,786,787],{"class":285}," points.getFeatures(\n",[276,789,791,794,797],{"class":102,"line":790},12,[276,792,793],{"class":285},"    QgsFeatureRequest().setFilterExpression(",[276,795,796],{"class":361},"\"$geometry IS NULL\"",[276,798,799],{"class":285},")))\n",[276,801,803,805,808,811,814,817],{"class":102,"line":802},13,[276,804,318],{"class":317},[276,806,807],{"class":285},"(points.featureCount(), ",[276,809,810],{"class":361},"\"rows,\"",[276,812,813],{"class":285},", empty, ",[276,815,816],{"class":361},"\"without geometry\"",[276,818,400],{"class":285},[14,820,821,823],{},[417,822,419],{}," Every parameter that affects the result is stated, so the same script gives the same layer on any machine and any locale. The null-geometry count is the number that tells you whether the parsing assumptions were right; if it is not zero, the reason is almost always a separator or a blank cell, and the rows can be listed by identifier.",[201,825,827],{"id":826},"containers-one-file-many-layers","Containers: one file, many layers",[14,829,830],{},"GeoPackage, File Geodatabase, KML, spreadsheets with several sheets and multi-variable NetCDF files are containers. Opening the file is not the same as opening a layer; the source string has to name which table, sheet or subdataset you mean, and hard-coding that name ties the script to whatever the supplier called it last time.",[14,832,833],{},[34,834,837,840,843,846,849,853,857,860,864,868,872,875,880,883,886,889],{"viewBox":835,"role":37,"ariaLabel":836,"xmlns":39},"0 0 760 256","Hard-coded layer names versus querying sublayers: a script that assumes a table name breaks when the supplier renames it, while a script that queries the container adapts",[41,838,839],{},"Ask the container, do not assume it",[45,841,842],{},"Left: a script opening base.gpkg with layername equals roads works in March, then the April delivery renames the table to road_links and the layer is invalid. Right: a script that queries sublayers and filters by geometry type and a name pattern finds road_links in April without a change.",[49,844],{"x":51,"y":51,"width":52,"height":845,"fill":54},"256",[73,847,848],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"The April delivery renamed a table",[49,850],{"x":83,"y":153,"width":851,"height":852,"rx":87,"fill":168,"stroke":496,"style":115},"344","186",[73,854,856],{"x":473,"y":855,"style":478,"fill":496,"textAnchor":79},"74","hard-coded name",[73,858,859],{"x":473,"y":93,"style":482,"fill":71,"textAnchor":79},"base.gpkg|layername=roads",[73,861,863],{"x":473,"y":862,"style":163,"fill":71,"textAnchor":79},"138","March: roads → valid",[73,865,867],{"x":473,"y":866,"style":163,"fill":71,"textAnchor":79},"164","April: road_links → invalid",[73,869,871],{"x":473,"y":870,"style":125,"fill":496,"textAnchor":79},"206","script fails, or worse, skips it",[49,873],{"x":874,"y":153,"width":851,"height":852,"rx":87,"fill":190,"stroke":191,"style":115},"392",[73,876,879],{"x":877,"y":855,"style":478,"fill":878,"textAnchor":79},"564","#166534","query sublayers",[73,881,882],{"x":877,"y":93,"style":482,"fill":71,"textAnchor":79},"querySublayers(path)",[73,884,885],{"x":877,"y":862,"style":163,"fill":71,"textAnchor":79},"filter: lines, name ~ road",[73,887,888],{"x":877,"y":866,"style":163,"fill":71,"textAnchor":79},"April: finds road_links",[73,890,891],{"x":877,"y":870,"style":125,"fill":878,"textAnchor":79},"adapts, and can log the change",[14,893,894,897,898,901,902,906],{},[18,895,896],{},"QgsProviderRegistry.querySublayers"," solves that. It asks every capable provider what the file contains and returns a details object per sublayer, carrying the name, type, geometry and a ready-to-use URI. Filtering those details and calling ",[18,899,900],{},"toLayer"," on the survivors loads exactly what you need, with the right provider, without ever opening the tables you skipped. ",[26,903,905],{"href":904},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Flist-and-load-geopackage-sublayers-pyqgis\u002F","Listing and loading GeoPackage sublayers"," covers fast scans versus counted queries, mixed vector and raster content, and the default styles a GeoPackage can carry.",[267,908,910],{"className":269,"code":909,"language":271,"meta":272,"style":272},"from qgis.core import (\n    Qgis, QgsProject, QgsProviderRegistry, QgsProviderSublayerDetails, QgsWkbTypes,\n)\n\npath = \"\u002Fdata\u002Fdeliveries\u002Fapril\u002Fbase.gpkg\"\noptions = QgsProviderSublayerDetails.LayerOptions(\n    QgsProject.instance().transformContext())\n\nroads = [d for d in QgsProviderRegistry.instance().querySublayers(path)\n         if d.type() == Qgis.LayerType.Vector\n         and \"road\" in d.name().lower()\n         and QgsWkbTypes.geometryType(d.wkbType()) == Qgis.GeometryType.Line]\nif len(roads) != 1:\n    raise RuntimeError(f\"expected one road table, found {[d.name() for d in roads]}\")\nroad_layer = roads[0].toLayer(options)\n",[18,911,912,923,928,932,936,946,956,961,965,985,999,1013,1025,1045,1081],{"__ignoreMap":272},[276,913,914,916,918,920],{"class":102,"line":278},[276,915,282],{"class":281},[276,917,286],{"class":285},[276,919,289],{"class":281},[276,921,922],{"class":285}," (\n",[276,924,925],{"class":102,"line":295},[276,926,927],{"class":285},"    Qgis, QgsProject, QgsProviderRegistry, QgsProviderSublayerDetails, QgsWkbTypes,\n",[276,929,930],{"class":102,"line":302},[276,931,400],{"class":285},[276,933,934],{"class":102,"line":314},[276,935,299],{"emptyLinePlaceholder":298},[276,937,938,941,943],{"class":102,"line":330},[276,939,940],{"class":285},"path ",[276,942,308],{"class":281},[276,944,945],{"class":361}," \"\u002Fdata\u002Fdeliveries\u002Fapril\u002Fbase.gpkg\"\n",[276,947,948,951,953],{"class":102,"line":335},[276,949,950],{"class":285},"options ",[276,952,308],{"class":281},[276,954,955],{"class":285}," QgsProviderSublayerDetails.LayerOptions(\n",[276,957,958],{"class":102,"line":350},[276,959,960],{"class":285},"    QgsProject.instance().transformContext())\n",[276,962,963],{"class":102,"line":403},[276,964,299],{"emptyLinePlaceholder":298},[276,966,967,970,972,975,977,980,982],{"class":102,"line":737},[276,968,969],{"class":285},"roads ",[276,971,308],{"class":281},[276,973,974],{"class":285}," [d ",[276,976,338],{"class":281},[276,978,979],{"class":285}," d ",[276,981,344],{"class":281},[276,983,984],{"class":285}," QgsProviderRegistry.instance().querySublayers(path)\n",[276,986,987,990,993,996],{"class":102,"line":758},[276,988,989],{"class":281},"         if",[276,991,992],{"class":285}," d.type() ",[276,994,995],{"class":281},"==",[276,997,998],{"class":285}," Qgis.LayerType.Vector\n",[276,1000,1001,1004,1007,1010],{"class":102,"line":763},[276,1002,1003],{"class":281},"         and",[276,1005,1006],{"class":361}," \"road\"",[276,1008,1009],{"class":281}," in",[276,1011,1012],{"class":285}," d.name().lower()\n",[276,1014,1015,1017,1020,1022],{"class":102,"line":790},[276,1016,1003],{"class":281},[276,1018,1019],{"class":285}," QgsWkbTypes.geometryType(d.wkbType()) ",[276,1021,995],{"class":281},[276,1023,1024],{"class":285}," Qgis.GeometryType.Line]\n",[276,1026,1027,1030,1033,1036,1039,1042],{"class":102,"line":802},[276,1028,1029],{"class":281},"if",[276,1031,1032],{"class":317}," len",[276,1034,1035],{"class":285},"(roads) ",[276,1037,1038],{"class":281},"!=",[276,1040,1041],{"class":317}," 1",[276,1043,1044],{"class":285},":\n",[276,1046,1048,1051,1054,1056,1058,1061,1063,1066,1068,1070,1072,1075,1077,1079],{"class":102,"line":1047},14,[276,1049,1050],{"class":281},"    raise",[276,1052,1053],{"class":317}," RuntimeError",[276,1055,321],{"class":285},[276,1057,358],{"class":281},[276,1059,1060],{"class":361},"\"expected one road table, found ",[276,1062,365],{"class":317},[276,1064,1065],{"class":285},"[d.name() ",[276,1067,338],{"class":281},[276,1069,979],{"class":285},[276,1071,344],{"class":281},[276,1073,1074],{"class":285}," roads]",[276,1076,374],{"class":317},[276,1078,362],{"class":361},[276,1080,400],{"class":285},[276,1082,1084,1087,1089,1092,1094],{"class":102,"line":1083},15,[276,1085,1086],{"class":285},"road_layer ",[276,1088,308],{"class":281},[276,1090,1091],{"class":285}," roads[",[276,1093,51],{"class":317},[276,1095,1096],{"class":285},"].toLayer(options)\n",[14,1098,1099,1101,1102,1105,1106,1109,1110,1114],{},[417,1100,419],{}," Filtering by geometry type as well as by name stops a ",[18,1103,1104],{},"road_names"," attribute table or a ",[18,1107,1108],{},"road_buffers"," polygon layer being picked up by accident. Insisting on exactly one match turns an ambiguous delivery into a clear error instead of a silently wrong choice — the right behaviour for ",[26,1111,1113],{"href":1112},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002Frun-algorithm-over-folder-of-files-pyqgis\u002F","a scheduled batch job"," that nobody is watching.",[14,1116,1117,1118,1122],{},"Writing into containers is the other half. Adding a table to an existing GeoPackage without overwriting the file needs the right writer options, which ",[26,1119,1121],{"href":1120},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fwrite-vector-layer-to-geopackage-pyqgis\u002F","writing a vector layer to GeoPackage"," covers in detail.",[201,1124,1126],{"id":1125},"views-sql-across-layers","Views: SQL across layers",[14,1128,1129,1130,1133],{},"Sometimes the data you need does not exist as a table anywhere: a count of incidents per ward, a spreadsheet of readings joined to station locations, a filtered union of two layers. The ",[18,1131,1132],{},"virtual"," provider builds that as a live SQL view over any loaded layers, using SQLite with SpatiaLite functions, and exposes it as an ordinary read-only layer.",[14,1135,1136],{},[34,1137,1140,1143,1146,1149,1152,1154,1157,1161,1164,1168,1172,1174,1177,1180,1183,1186,1189,1191,1194,1197,1200,1203],{"viewBox":1138,"role":37,"ariaLabel":1139,"xmlns":39},"0 0 760 262","Three ways to get a SQL result: a virtual layer keeps a live view, Execute SQL writes a one-off output, and a database query runs where the data already lives, arranged by freshness and speed",[41,1141,1142],{},"Live view, one-off output, or the database",[45,1144,1145],{},"A virtual layer re-runs its query on every read, so it is always current but can be slow to pan. The Execute SQL algorithm runs the same kind of query once and writes a file, so it is fast to use but a snapshot. A query in PostGIS runs next to the data and is fastest for large tables, but only when every source is already in the database.",[49,1147],{"x":51,"y":51,"width":52,"height":1148,"fill":54},"262",[73,1150,1151],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"Where the query runs decides what you get",[49,1153],{"x":83,"y":471,"width":472,"height":500,"rx":87,"fill":190,"stroke":191,"style":115},[73,1155,1156],{"x":476,"y":477,"style":478,"fill":878,"textAnchor":79},"virtual layer",[73,1158,1160],{"x":476,"y":1159,"style":163,"fill":71,"textAnchor":79},"106","re-runs on every read",[73,1162,1163],{"x":476,"y":84,"style":163,"fill":71,"textAnchor":79},"mixes any providers",[73,1165,1167],{"x":476,"y":1166,"style":163,"fill":71,"textAnchor":79},"154","always current",[73,1169,1171],{"x":476,"y":1170,"style":125,"fill":89,"textAnchor":79},"214","best for a live dashboard layer",[49,1173],{"x":504,"y":471,"width":472,"height":500,"rx":87,"fill":113,"stroke":114,"style":115},[73,1175,1176],{"x":75,"y":477,"style":478,"fill":114,"textAnchor":79},"Execute SQL",[73,1178,1179],{"x":75,"y":1159,"style":163,"fill":71,"textAnchor":79},"runs once",[73,1181,1182],{"x":75,"y":84,"style":163,"fill":71,"textAnchor":79},"writes a real output",[73,1184,1185],{"x":75,"y":1166,"style":163,"fill":71,"textAnchor":79},"a snapshot",[73,1187,1188],{"x":75,"y":1170,"style":125,"fill":89,"textAnchor":79},"best inside a pipeline",[49,1190],{"x":526,"y":471,"width":472,"height":500,"rx":87,"fill":156,"stroke":157,"style":115},[73,1192,1193],{"x":529,"y":477,"style":478,"fill":157,"textAnchor":79},"PostGIS query",[73,1195,1196],{"x":529,"y":1159,"style":163,"fill":71,"textAnchor":79},"runs next to the data",[73,1198,1199],{"x":529,"y":84,"style":163,"fill":71,"textAnchor":79},"uses database indexes",[73,1201,1202],{"x":529,"y":1166,"style":163,"fill":71,"textAnchor":79},"fastest on big tables",[73,1204,1205],{"x":529,"y":1170,"style":125,"fill":89,"textAnchor":79},"when everything lives there",[14,1207,1208,1209,1212,1213,1217,1218,428,1221,1225],{},"Virtual layers are powerful and easy to make slow. The query runs every time the layer is read — on every pan, identify and attribute table refresh — so declaring the unique id and geometry up front, and using the ",[18,1210,1211],{},"_search_frame_"," column to push spatial joins onto an index, is the difference between an instant layer and a frozen canvas. ",[26,1214,1216],{"href":1215},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fquery-layers-with-virtual-layer-sql-pyqgis\u002F","Querying layers with virtual layer SQL"," builds definitions in Python, reads them back from saved projects, and shows when ",[1219,1220,1176],"em",{},[26,1222,1224],{"href":1223},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fexecute-sql-on-postgis-pyqgis\u002F","a query in PostGIS"," is the better choice.",[267,1227,1229],{"className":269,"code":1228,"language":271,"meta":272,"style":272},"from qgis.core import QgsVectorLayer, QgsVirtualLayerDefinition\n\ndefinition = QgsVirtualLayerDefinition()\ndefinition.setQuery(\"\"\"\n    SELECT s.fid AS station_id, s.name, max(r.pm25) AS peak_pm25, s.geometry\n    FROM stations AS s JOIN readings AS r ON r.station = s.code\n    GROUP BY s.fid\n\"\"\")\ndefinition.setUid(\"station_id\")\ndefinition.setGeometryField(\"geometry\")\npeaks = QgsVectorLayer(definition.toString(), \"peak PM2.5\", \"virtual\")\nprint(peaks.isValid(), peaks.featureCount())\n",[18,1230,1231,1242,1246,1256,1264,1269,1274,1279,1286,1296,1306,1326],{"__ignoreMap":272},[276,1232,1233,1235,1237,1239],{"class":102,"line":278},[276,1234,282],{"class":281},[276,1236,286],{"class":285},[276,1238,289],{"class":281},[276,1240,1241],{"class":285}," QgsVectorLayer, QgsVirtualLayerDefinition\n",[276,1243,1244],{"class":102,"line":295},[276,1245,299],{"emptyLinePlaceholder":298},[276,1247,1248,1251,1253],{"class":102,"line":302},[276,1249,1250],{"class":285},"definition ",[276,1252,308],{"class":281},[276,1254,1255],{"class":285}," QgsVirtualLayerDefinition()\n",[276,1257,1258,1261],{"class":102,"line":314},[276,1259,1260],{"class":285},"definition.setQuery(",[276,1262,1263],{"class":361},"\"\"\"\n",[276,1265,1266],{"class":102,"line":330},[276,1267,1268],{"class":361},"    SELECT s.fid AS station_id, s.name, max(r.pm25) AS peak_pm25, s.geometry\n",[276,1270,1271],{"class":102,"line":335},[276,1272,1273],{"class":361},"    FROM stations AS s JOIN readings AS r ON r.station = s.code\n",[276,1275,1276],{"class":102,"line":350},[276,1277,1278],{"class":361},"    GROUP BY s.fid\n",[276,1280,1281,1284],{"class":102,"line":403},[276,1282,1283],{"class":361},"\"\"\"",[276,1285,400],{"class":285},[276,1287,1288,1291,1294],{"class":102,"line":737},[276,1289,1290],{"class":285},"definition.setUid(",[276,1292,1293],{"class":361},"\"station_id\"",[276,1295,400],{"class":285},[276,1297,1298,1301,1304],{"class":102,"line":758},[276,1299,1300],{"class":285},"definition.setGeometryField(",[276,1302,1303],{"class":361},"\"geometry\"",[276,1305,400],{"class":285},[276,1307,1308,1311,1313,1316,1319,1321,1324],{"class":102,"line":763},[276,1309,1310],{"class":285},"peaks ",[276,1312,308],{"class":281},[276,1314,1315],{"class":285}," QgsVectorLayer(definition.toString(), ",[276,1317,1318],{"class":361},"\"peak PM2.5\"",[276,1320,258],{"class":285},[276,1322,1323],{"class":361},"\"virtual\"",[276,1325,400],{"class":285},[276,1327,1328,1330],{"class":102,"line":790},[276,1329,318],{"class":317},[276,1331,1332],{"class":285},"(peaks.isValid(), peaks.featureCount())\n",[14,1334,1335,420,1337,222,1340,1343],{},[417,1336,419],{},[18,1338,1339],{},"stations",[18,1341,1342],{},"readings"," are the names of layers in the project — one a point layer from a database, the other a geometryless CSV — and the provider resolves them at open time. The integer uid keeps feature ids stable, so a selection on the map survives a refresh.",[201,1345,1347],{"id":1346},"when-the-data-moves","When the data moves",[14,1349,1350],{},"Projects store directions to data, not data. That is what makes them small and shareable, and it is also why they break when a file server is reorganised, a drive letter changes or a database moves to a new host. QGIS Desktop offers a dialog to fix unavailable layers one project at a time; at the scale of a migration, you need the same repair as a script.",[14,1352,1353],{},[34,1354,1357,1360,1363,1366,1373,1376,1381,1385,1388,1391,1394,1398,1402,1405,1409,1413,1417,1420,1423,1426,1429,1432],{"viewBox":1355,"role":37,"ariaLabel":1356,"xmlns":39},"0 0 760 244","Before and after a file server migration: projects storing absolute paths break, projects storing relative paths next to their data keep working, and a repair script converts the first kind into the second",[41,1358,1359],{},"Repair once, then make it portable",[45,1361,1362],{},"A timeline: projects saved with absolute S drive paths break after the migration. The repair script reads them without resolving layers, remaps the prefixes, and saves them with relative path storage. The next move of the project folder, together with its data, does not break anything.",[49,1364],{"x":51,"y":51,"width":52,"height":1365,"fill":54},"244",[56,1367,1368],{},[59,1369,1371],{"id":1370,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"ldsMoveArrow",[68,1372],{"d":70,"fill":71},[73,1374,1375],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"A migration should only hurt once",[102,1377],{"x1":155,"y1":84,"x2":1378,"y2":84,"stroke":1379,"style":1380},"720","#9aa39d","stroke-width:3",[1382,1383],"circle",{"cx":84,"cy":84,"r":1384,"fill":191},"9",[1382,1386],{"cx":1387,"cy":84,"r":1384,"fill":496},"310",[1382,1389],{"cx":1390,"cy":84,"r":1384,"fill":114},"490",[1382,1392],{"cx":1393,"cy":84,"r":1384,"fill":191},"660",[49,1395],{"x":471,"y":1396,"width":85,"height":1397,"rx":63,"fill":88,"stroke":89,"style":158},"52","56",[73,1399,1401],{"x":84,"y":477,"style":1400,"fill":78,"textAnchor":79},"text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","projects work",[73,1403,1404],{"x":84,"y":112,"style":99,"fill":71,"textAnchor":79},"S:\u002Fgis\u002F…",[49,1406],{"x":1407,"y":1408,"width":85,"height":1397,"rx":63,"fill":168,"stroke":496,"style":158},"230","152",[73,1410,1412],{"x":1387,"y":1411,"style":1400,"fill":496,"textAnchor":79},"176","migration",[73,1414,1416],{"x":1387,"y":473,"style":1415,"fill":71,"textAnchor":79},"text-anchor:middle;font-size:9.5px;font-family:sans-serif","every layer unavailable",[49,1418],{"x":1419,"y":1396,"width":85,"height":1397,"rx":63,"fill":113,"stroke":114,"style":158},"410",[73,1421,1422],{"x":1390,"y":477,"style":1400,"fill":114,"textAnchor":79},"repair script",[73,1424,1425],{"x":1390,"y":112,"style":1415,"fill":71,"textAnchor":79},"remap, save relative",[49,1427],{"x":1428,"y":1408,"width":85,"height":1397,"rx":63,"fill":190,"stroke":191,"style":158},"580",[73,1430,1431],{"x":1393,"y":1411,"style":1400,"fill":878,"textAnchor":79},"next move",[73,1433,1434],{"x":1393,"y":473,"style":1415,"fill":71,"textAnchor":79},"nothing breaks",[14,1436,1437,1438,1441,1442,1446],{},"The repair has a clear shape. Read each project with layer resolution turned off so nothing tries to open the missing data; decode each layer's source; apply an ordered table of old-to-new prefixes; confirm the new path exists; and apply it with ",[18,1439,1440],{},"setDataSource",", which keeps styles, joins, relations and layout references attached. ",[26,1443,1445],{"href":1444},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Ffix-broken-layer-paths-in-project-pyqgis\u002F","Fixing broken layer paths in a project"," builds that script, adds a dry-run audit across a whole folder of projects, and saves the result with relative path storage.",[267,1448,1450],{"className":269,"code":1449,"language":271,"meta":272,"style":272},"from qgis.core import Qgis, QgsProject\n\nproject = QgsProject.instance()\nproject.read(\"\u002Fsrv\u002Fprojects\u002Fflood.qgz\", Qgis.ProjectReadFlag.DontResolveLayers)\nunresolved = {\n    layer.name(): registry.decodeUri(layer.providerType(), layer.source()).get(\"path\")\n    for layer in project.mapLayers().values()\n}\nprint(unresolved)\n",[18,1451,1452,1463,1467,1477,1488,1498,1508,1520,1524],{"__ignoreMap":272},[276,1453,1454,1456,1458,1460],{"class":102,"line":278},[276,1455,282],{"class":281},[276,1457,286],{"class":285},[276,1459,289],{"class":281},[276,1461,1462],{"class":285}," Qgis, QgsProject\n",[276,1464,1465],{"class":102,"line":295},[276,1466,299],{"emptyLinePlaceholder":298},[276,1468,1469,1472,1474],{"class":102,"line":302},[276,1470,1471],{"class":285},"project ",[276,1473,308],{"class":281},[276,1475,1476],{"class":285}," QgsProject.instance()\n",[276,1478,1479,1482,1485],{"class":102,"line":314},[276,1480,1481],{"class":285},"project.read(",[276,1483,1484],{"class":361},"\"\u002Fsrv\u002Fprojects\u002Fflood.qgz\"",[276,1486,1487],{"class":285},", Qgis.ProjectReadFlag.DontResolveLayers)\n",[276,1489,1490,1493,1495],{"class":102,"line":330},[276,1491,1492],{"class":285},"unresolved ",[276,1494,308],{"class":281},[276,1496,1497],{"class":285}," {\n",[276,1499,1500,1503,1506],{"class":102,"line":335},[276,1501,1502],{"class":285},"    layer.name(): registry.decodeUri(layer.providerType(), layer.source()).get(",[276,1504,1505],{"class":361},"\"path\"",[276,1507,400],{"class":285},[276,1509,1510,1513,1515,1517],{"class":102,"line":350},[276,1511,1512],{"class":281},"    for",[276,1514,341],{"class":285},[276,1516,344],{"class":281},[276,1518,1519],{"class":285}," project.mapLayers().values()\n",[276,1521,1522],{"class":102,"line":403},[276,1523,696],{"class":285},[276,1525,1526,1528],{"class":102,"line":737},[276,1527,318],{"class":317},[276,1529,1530],{"class":285},"(unresolved)\n",[14,1532,1533,1535],{},[417,1534,419],{}," Reading without resolving is fast even when the share is gone, because no provider opens anything; every layer is a placeholder that still carries its full definition. The dictionary it produces is the input to the remapping step, and printing it before changing anything is the cheapest possible audit of what a project depends on.",[201,1537,1539],{"id":1538},"provider-capabilities-decide-what-a-script-may-do","Provider capabilities decide what a script may do",[14,1541,1542,1543,1545],{},"Loading a layer is only the start; what you can do with it next depends on the provider behind it. A ",[18,1544,229],{}," layer cannot be edited. A WFS layer can be edited only if the server supports transactions. A virtual layer cannot add fields. A GeoPackage layer can do nearly everything, and a shapefile can do most things but will silently truncate a new field name to ten characters. Scripts that assume every layer behaves like a GeoPackage fail half-way through a batch, after some changes have been written.",[14,1547,1548],{},"Every data provider reports what it supports as a set of capability flags, and checking them before starting work turns those mid-batch failures into a clear message up front.",[14,1550,1551],{},[34,1552,1555,1558,1561,1564,1568,1571,1575,1579,1583,1587,1589,1593,1596,1598,1600,1604,1606,1608,1611,1614,1617,1619,1621,1624,1627,1629,1631],{"viewBox":1553,"role":37,"ariaLabel":1554,"xmlns":39},"0 0 760 236","A capability matrix for common providers: GeoPackage supports adding features, editing attributes and adding fields; shapefile supports them with field name limits; CSV and virtual layers support none; WFS supports editing only with a transactional server",[41,1556,1557],{},"What each provider lets a script do",[45,1559,1560],{},"Rows for ogr GeoPackage, ogr shapefile, delimitedtext, virtual and WFS. Columns for add features, edit attributes and add fields. GeoPackage has all three. Shapefile has all three with a ten-character field name limit. Delimitedtext and virtual have none. WFS has the first two only when the server supports WFS-T and none otherwise.",[49,1562],{"x":51,"y":51,"width":52,"height":1563,"fill":54},"236",[73,1565,1567],{"x":75,"y":1566,"style":77,"fill":78,"textAnchor":79},"26","Check before you edit, not after",[49,1569],{"x":83,"y":155,"width":1570,"height":124,"rx":87,"fill":88,"stroke":89,"style":90},"712",[73,1572,1574],{"x":134,"y":1573,"style":1400,"fill":78,"textAnchor":79},"66","provider",[73,1576,1578],{"x":1577,"y":1573,"style":1400,"fill":78,"textAnchor":79},"360","add features",[73,1580,1582],{"x":1581,"y":1573,"style":1400,"fill":78,"textAnchor":79},"500","edit attributes",[73,1584,1586],{"x":1585,"y":1573,"style":1400,"fill":78,"textAnchor":79},"640","add fields",[102,1588],{"x1":155,"y1":477,"x2":1378,"y2":477,"stroke":491,"style":492},[73,1590,1592],{"x":134,"y":1591,"style":163,"fill":71,"textAnchor":79},"100","GeoPackage",[73,1594,1595],{"x":1577,"y":1591,"style":94,"fill":878,"textAnchor":79},"yes",[73,1597,1595],{"x":1581,"y":1591,"style":94,"fill":878,"textAnchor":79},[73,1599,1595],{"x":1585,"y":1591,"style":94,"fill":878,"textAnchor":79},[73,1601,1603],{"x":134,"y":1602,"style":163,"fill":71,"textAnchor":79},"128","shapefile",[73,1605,1595],{"x":1577,"y":1602,"style":94,"fill":878,"textAnchor":79},[73,1607,1595],{"x":1581,"y":1602,"style":94,"fill":878,"textAnchor":79},[73,1609,1610],{"x":1585,"y":1602,"style":1400,"fill":169,"textAnchor":79},"10-char names",[73,1612,1613],{"x":134,"y":119,"style":163,"fill":71,"textAnchor":79},"delimitedtext · virtual",[73,1615,1616],{"x":1577,"y":119,"style":94,"fill":496,"textAnchor":79},"no",[73,1618,1616],{"x":1581,"y":119,"style":94,"fill":496,"textAnchor":79},[73,1620,1616],{"x":1585,"y":119,"style":94,"fill":496,"textAnchor":79},[73,1622,1623],{"x":134,"y":104,"style":163,"fill":71,"textAnchor":79},"WFS",[73,1625,1626],{"x":1577,"y":104,"style":1400,"fill":169,"textAnchor":79},"WFS-T only",[73,1628,1626],{"x":1581,"y":104,"style":1400,"fill":169,"textAnchor":79},[73,1630,1616],{"x":1585,"y":104,"style":94,"fill":496,"textAnchor":79},[73,1632,1634],{"x":75,"y":1633,"style":1415,"fill":89,"textAnchor":79},"210","the flags come from the provider at runtime — trust them over this table",[267,1636,1638],{"className":269,"code":1637,"language":271,"meta":272,"style":272},"from qgis.core import QgsVectorDataProvider\n\ndef require(layer, *caps):\n    provider = layer.dataProvider()\n    have = provider.capabilities()\n    missing = [name for name, cap in caps if not have & cap]\n    if missing:\n        raise RuntimeError(\n            f\"{layer.name()} ({layer.providerType()}) cannot: {', '.join(missing)}. \"\n            f\"It supports: {provider.capabilitiesString()}\"\n        )\n\nrequire(points,\n        (\"add features\", QgsVectorDataProvider.AddFeatures),\n        (\"change attribute values\", QgsVectorDataProvider.ChangeAttributeValues),\n        (\"add fields\", QgsVectorDataProvider.AddAttributes))\n",[18,1639,1640,1651,1655,1673,1683,1693,1727,1735,1745,1783,1799,1804,1808,1813,1824,1834],{"__ignoreMap":272},[276,1641,1642,1644,1646,1648],{"class":102,"line":278},[276,1643,282],{"class":281},[276,1645,286],{"class":285},[276,1647,289],{"class":281},[276,1649,1650],{"class":285}," QgsVectorDataProvider\n",[276,1652,1653],{"class":102,"line":295},[276,1654,299],{"emptyLinePlaceholder":298},[276,1656,1657,1660,1664,1667,1670],{"class":102,"line":302},[276,1658,1659],{"class":281},"def",[276,1661,1663],{"class":1662},"svObZ"," require",[276,1665,1666],{"class":285},"(layer, ",[276,1668,1669],{"class":281},"*",[276,1671,1672],{"class":285},"caps):\n",[276,1674,1675,1678,1680],{"class":102,"line":314},[276,1676,1677],{"class":285},"    provider ",[276,1679,308],{"class":281},[276,1681,1682],{"class":285}," layer.dataProvider()\n",[276,1684,1685,1688,1690],{"class":102,"line":330},[276,1686,1687],{"class":285},"    have ",[276,1689,308],{"class":281},[276,1691,1692],{"class":285}," provider.capabilities()\n",[276,1694,1695,1698,1700,1703,1705,1708,1710,1713,1715,1718,1721,1724],{"class":102,"line":335},[276,1696,1697],{"class":285},"    missing ",[276,1699,308],{"class":281},[276,1701,1702],{"class":285}," [name ",[276,1704,338],{"class":281},[276,1706,1707],{"class":285}," name, cap ",[276,1709,344],{"class":281},[276,1711,1712],{"class":285}," caps ",[276,1714,1029],{"class":281},[276,1716,1717],{"class":281}," not",[276,1719,1720],{"class":285}," have ",[276,1722,1723],{"class":281},"&",[276,1725,1726],{"class":285}," cap]\n",[276,1728,1729,1732],{"class":102,"line":350},[276,1730,1731],{"class":281},"    if",[276,1733,1734],{"class":285}," missing:\n",[276,1736,1737,1740,1742],{"class":102,"line":403},[276,1738,1739],{"class":281},"        raise",[276,1741,1053],{"class":317},[276,1743,1744],{"class":285},"(\n",[276,1746,1747,1750,1752,1754,1756,1758,1761,1763,1765,1767,1770,1772,1775,1778,1780],{"class":102,"line":737},[276,1748,1749],{"class":281},"            f",[276,1751,362],{"class":361},[276,1753,365],{"class":317},[276,1755,368],{"class":285},[276,1757,374],{"class":317},[276,1759,1760],{"class":361}," (",[276,1762,365],{"class":317},[276,1764,380],{"class":285},[276,1766,374],{"class":317},[276,1768,1769],{"class":361},") cannot: ",[276,1771,365],{"class":317},[276,1773,1774],{"class":361},"', '",[276,1776,1777],{"class":285},".join(missing)",[276,1779,374],{"class":317},[276,1781,1782],{"class":361},". \"\n",[276,1784,1785,1787,1790,1792,1795,1797],{"class":102,"line":758},[276,1786,1749],{"class":281},[276,1788,1789],{"class":361},"\"It supports: ",[276,1791,365],{"class":317},[276,1793,1794],{"class":285},"provider.capabilitiesString()",[276,1796,374],{"class":317},[276,1798,734],{"class":361},[276,1800,1801],{"class":102,"line":763},[276,1802,1803],{"class":285},"        )\n",[276,1805,1806],{"class":102,"line":790},[276,1807,299],{"emptyLinePlaceholder":298},[276,1809,1810],{"class":102,"line":802},[276,1811,1812],{"class":285},"require(points,\n",[276,1814,1815,1818,1821],{"class":102,"line":1047},[276,1816,1817],{"class":285},"        (",[276,1819,1820],{"class":361},"\"add features\"",[276,1822,1823],{"class":285},", QgsVectorDataProvider.AddFeatures),\n",[276,1825,1826,1828,1831],{"class":102,"line":1083},[276,1827,1817],{"class":285},[276,1829,1830],{"class":361},"\"change attribute values\"",[276,1832,1833],{"class":285},", QgsVectorDataProvider.ChangeAttributeValues),\n",[276,1835,1837,1839,1842],{"class":102,"line":1836},16,[276,1838,1817],{"class":285},[276,1840,1841],{"class":361},"\"add fields\"",[276,1843,1844],{"class":285},", QgsVectorDataProvider.AddAttributes))\n",[14,1846,1847,420,1849,1852,1853,1856,1857,1860,1861,1864,1865,1868,1869,1873],{},[417,1848,419],{},[18,1850,1851],{},"capabilities()"," returns a bit mask, and testing each required flag against it tells you before any edit whether the layer can accept it. For the CSV layer loaded earlier in this guide the check fails immediately on ",[18,1854,1855],{},"AddFeatures",", which is the prompt to copy it into GeoPackage. ",[18,1858,1859],{},"capabilitiesString()"," gives a human-readable list of everything the provider does support, which is worth logging once per layer in unattended jobs. The same idea applies to spatial filtering: providers that report ",[18,1862,1863],{},"SelectAtId"," and a native spatial index answer ",[18,1866,1867],{},"QgsFeatureRequest"," filters quickly, while a provider without them falls back to reading everything, which is why ",[26,1870,1872],{"href":1871},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fspeed-up-feature-iteration-with-qgsfeaturerequest-pyqgis\u002F","speeding up feature iteration"," starts by asking where the data lives.",[201,1875,1877],{"id":1876},"choosing-the-right-route-in","Choosing the right route in",[14,1879,1880],{},"Most format decisions come down to three questions: whether the data should stay where it is, whether it will be edited, and whether a script will run against it repeatedly.",[1882,1883,1884,1894,1900,1906,1914],"ul",{},[1885,1886,1887,1890,1891,1893],"li",{},[417,1888,1889],{},"Keep it in place, read it often:"," load it directly — a ",[18,1892,229],{}," layer over a CSV, a sublayer from a GeoPackage, a WFS. The source stays authoritative.",[1885,1895,1896,1899],{},[417,1897,1898],{},"Edit it, or analyse it seriously:"," copy it into a GeoPackage first. You gain real field types, a persistent spatial index and edit support, and you stop depending on re-detection of a text file.",[1885,1901,1902,1905],{},[417,1903,1904],{},"Combine several sources without copying:"," a virtual layer, accepting that it recomputes on read.",[1885,1907,1908,420,1911,1913],{},[417,1909,1910],{},"Combine and keep the answer:",[1219,1912,1176],{}," or a dedicated Processing algorithm, writing a real output.",[1885,1915,1916,1919,1920,1924,1925,1929],{},[417,1917,1918],{},"Data that lives on a server:"," let the server do the work where it can — ",[26,1921,1923],{"href":1922},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fload-postgis-query-layer-pyqgis\u002F","PostGIS queries",", or filters pushed into ",[26,1926,1928],{"href":1927},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fload-wfs-layer-pyqgis\u002F","WFS requests",".",[14,1931,1932],{},[34,1933,1936,1939,1942,1945,1952,1955,1957,1962,1967,1969,1972,1975,1978,1982,1986,1989,1993,1995,1999,2003,2007,2011,2015,2017,2019,2022,2024,2026],{"viewBox":1934,"role":37,"ariaLabel":1935,"xmlns":39},"0 0 760 250","A decision path for choosing how to bring data in, based on whether it will be edited, whether several sources are combined, and whether the result must stay live",[41,1937,1938],{},"Pick the route from what happens next",[45,1940,1941],{},"Start from the incoming data. If it will be edited or analysed heavily, copy it to GeoPackage. If not, and several sources must be combined, choose a virtual layer when the result must stay live, or Execute SQL when a snapshot is enough. If it is a single source that is only read, load it in place.",[49,1943],{"x":51,"y":51,"width":52,"height":1944,"fill":54},"250",[56,1946,1947],{},[59,1948,1950],{"id":1949,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"ldsPickArrow",[68,1951],{"d":70,"fill":71},[73,1953,1954],{"x":75,"y":1566,"style":77,"fill":78,"textAnchor":79},"Decide by what the data is for",[49,1956],{"x":83,"y":112,"width":84,"height":471,"rx":63,"fill":88,"stroke":89,"style":90},[73,1958,1961],{"x":1959,"y":1960,"style":94,"fill":78,"textAnchor":79},"89","126","incoming data",[102,1963],{"x1":1166,"y1":1964,"x2":1965,"y2":1964,"stroke":71,"style":1966},"121","188","stroke-width:1.8;marker-end:url(#ldsPickArrow)",[49,1968],{"x":473,"y":112,"width":134,"height":471,"rx":63,"fill":168,"stroke":169,"style":90},[73,1970,1971],{"x":149,"y":1960,"style":163,"fill":71,"textAnchor":79},"edited or analysed?",[102,1973],{"x1":149,"y1":112,"x2":149,"y2":1974,"stroke":71,"style":1966},"72",[49,1976],{"x":473,"y":155,"width":134,"height":76,"rx":1977,"fill":190,"stroke":191,"style":158},"6",[73,1979,1981],{"x":149,"y":1980,"style":125,"fill":878,"textAnchor":79},"59","yes: copy to GeoPackage",[102,1983],{"x1":1984,"y1":1964,"x2":1985,"y2":1964,"stroke":71,"style":1966},"336","370",[49,1987],{"x":1988,"y":112,"width":176,"height":471,"rx":63,"fill":156,"stroke":157,"style":90},"378",[73,1990,1992],{"x":1991,"y":1960,"style":163,"fill":71,"textAnchor":79},"453","several sources?",[102,1994],{"x1":1991,"y1":490,"x2":1991,"y2":852,"stroke":71,"style":1966},[49,1996],{"x":1988,"y":1997,"width":176,"height":1998,"rx":1977,"fill":88,"stroke":89,"style":158},"194","34",[73,2000,2002],{"x":1991,"y":2001,"style":125,"fill":71,"textAnchor":79},"215","no: load in place",[102,2004],{"x1":2005,"y1":1964,"x2":2006,"y2":1964,"stroke":71,"style":1966},"528","562",[49,2008],{"x":2009,"y":112,"width":2010,"height":471,"rx":63,"fill":113,"stroke":114,"style":90},"570","166",[73,2012,2014],{"x":2013,"y":1960,"style":163,"fill":71,"textAnchor":79},"653","must it stay live?",[102,2016],{"x1":2013,"y1":112,"x2":2013,"y2":1974,"stroke":71,"style":1966},[49,2018],{"x":2009,"y":155,"width":2010,"height":76,"rx":1977,"fill":190,"stroke":191,"style":158},[73,2020,2021],{"x":2013,"y":1980,"style":125,"fill":878,"textAnchor":79},"yes: virtual layer",[102,2023],{"x1":2013,"y1":490,"x2":2013,"y2":852,"stroke":71,"style":1966},[49,2025],{"x":2009,"y":1997,"width":2010,"height":1998,"rx":1977,"fill":88,"stroke":89,"style":158},[73,2027,2028],{"x":2013,"y":2001,"style":125,"fill":71,"textAnchor":79},"no: Execute SQL output",[14,2030,2031],{},"The order of those questions matters. Editing trumps everything else, because a read-only provider cannot be made writable after the fact and discovering that half-way through a field campaign is expensive. Combining sources comes next, because it determines whether a single provider is even involved. Only then does freshness decide between a view and a snapshot. A script that encodes the same order — checking capabilities first, then counting sources, then choosing a view or an output — makes the same decision every time a new dataset arrives, which is ultimately what separates an automated pipeline from a collection of one-off imports.",[201,2033,2035],{"id":2034},"key-takeaways","Key takeaways",[1882,2037,2038,2044,2050,2055,2062,2068],{},[1885,2039,2040,2041,2043],{},"A layer is a provider key plus a source string; always check ",[18,2042,244],{}," after creating one.",[1885,2045,2046,2047,2049],{},"Decode and encode source strings with the provider registry and ",[18,2048,264],{},", never with string splitting.",[1885,2051,2052,2054],{},[18,2053,229],{}," never rejects a row — count null geometries after every CSV load.",[1885,2056,2057,2058,2061],{},"Query containers with ",[18,2059,2060],{},"querySublayers"," and filter by type, geometry and name instead of hard-coding table names.",[1885,2063,2064,2065,2067],{},"Virtual layers are live SQL views; declare the uid and geometry and use ",[18,2066,1211],{}," for spatial joins.",[1885,2069,2070,2071,2073],{},"Repair moved projects by reading them unresolved, remapping prefixes, applying ",[18,2072,1440],{},", and saving with relative paths.",[201,2075,2077],{"id":2076},"frequently-asked-questions","Frequently Asked Questions",[14,2079,2080,2083],{},[417,2081,2082],{},"Which format should intermediate results use?","\nGeoPackage for anything you keep, memory layers for anything you discard at the end of the script. Shapefiles truncate field names to ten characters, cap file size at 2 GB, cannot store null dates properly and split into several files that travel badly by email; CSV loses types and geometry precision. FlatGeobuf is a good choice for large read-only outputs that will be streamed over HTTP, and GeoParquet for handing data to analytics tools outside QGIS where your GDAL build supports it.",[14,2085,2086,2089,2090,2093,2094,2097],{},[417,2087,2088],{},"Why is my layer valid but empty?","\nThe source opened but a subset string, a wrong ",[18,2091,2092],{},"layername",", or a query returning no rows filtered everything out. Print ",[18,2095,2096],{},"layer.subsetString()"," and decode the source to see which.",[14,2099,2100,2103,2104,2106],{},[417,2101,2102],{},"Can a layer switch provider without being removed?","\nYes. ",[18,2105,1440],{}," takes a provider key, so a shapefile layer can be repointed at a PostGIS table and keep its style, provided the geometry type and fields are compatible.",[14,2108,2109,2112,2113,2116],{},[417,2110,2111],{},"Do these techniques work in standalone scripts and QGIS Server?","\nYes. Providers, the registry and project reading are all part of ",[18,2114,2115],{},"qgis.core"," and need no GUI.",[14,2118,2119,2122,2123,2126],{},[417,2120,2121],{},"How do I know which parameters a provider's URI accepts?","\nBuild a layer the way you want it in the Data Source Manager, then print ",[18,2124,2125],{},"layer.source()"," and decode it. That is the most reliable documentation for your installed version.",[201,2128,2130],{"id":2129},"related","Related",[1882,2132,2133,2139,2144,2149,2154,2159,2164,2169,2175],{},[1885,2134,2135,2138],{},[26,2136,2137],{"href":28},"Spatial Data Processing & Automation with PyQGIS"," — the section this guide belongs to",[1885,2140,2141],{},[26,2142,2143],{"href":552},"Load a CSV as a Point Layer in PyQGIS",[1885,2145,2146],{},[26,2147,2148],{"href":252},"Decode and Build Data Source URIs in PyQGIS",[1885,2150,2151],{},[26,2152,2153],{"href":1444},"Fix Broken Layer Paths in a Project with PyQGIS",[1885,2155,2156],{},[26,2157,2158],{"href":1215},"Query Layers with Virtual Layer SQL in PyQGIS",[1885,2160,2161],{},[26,2162,2163],{"href":904},"List and Load GeoPackage Sublayers in PyQGIS",[1885,2165,2166],{},[26,2167,2168],{"href":564},"Import Geotagged Photos as Points in PyQGIS",[1885,2170,2171],{},[26,2172,2174],{"href":2173},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002F","PostGIS and Database Workflows in PyQGIS",[1885,2176,2177],{},[26,2178,2180],{"href":2179},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002F","Web Services and Remote Data in PyQGIS",[2182,2183,2184],"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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":272,"searchDepth":295,"depth":295,"links":2186},[2187,2188,2189,2190,2191,2192,2193,2194,2195,2196],{"id":203,"depth":295,"text":204},{"id":443,"depth":295,"text":444},{"id":826,"depth":295,"text":827},{"id":1125,"depth":295,"text":1126},{"id":1346,"depth":295,"text":1347},{"id":1538,"depth":295,"text":1539},{"id":1876,"depth":295,"text":1877},{"id":2034,"depth":295,"text":2035},{"id":2076,"depth":295,"text":2077},{"id":2129,"depth":295,"text":2130},"How PyQGIS layers reach their data — providers, source URIs and containers — with practical recipes for CSVs, GeoPackage sublayers, virtual layer SQL, geotagged photos and repairing projects whose data has moved.","md",{"slug":2200,"type":2201,"breadcrumb":2202,"datePublished":2203,"dateModified":2203},"layer-data-sources-and-formats","guide","Layer Data Sources & Formats","2026-09-17","\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats",{"title":5,"description":2197},"spatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Findex","1FrNkNpIBEYtZJQP83yKz3BX9gli8z4wDAYjZDqTjmk",1789632908424]