I have been using a great package called rpostgis as an interface to my PostGIS database to read and write rasters. As much as I enjoy using the package, I do have one reservation with it: its use of raster and proj4strings. To better understand how the package works and to find a way to use stars objects in place of raster in the reading and writing of PostGIS rasters, I went on to find a way to read PostGIS rasters into R as stars objects with the function pg_read_stars.

pg_read_stars = function(conn, layer, rast = "rast"){
  # reads raster from postgis database
  # conn: DBIConnection object
  # layer: name of table with raster 
  # rast: name of column that contains raster 
  
  library(stars)
  library(purrr)
  library(DBI)
  library(glue)
  
  values = dbGetQuery(
    conn,
    glue(
      "with ordered as (
          select 
          {rast} as rast, 
          rid from {layer} 
          order by rid asc)
       select
       unnest(st_dumpvalues(rast,1)) as values,
       row_number() over (order by rid) as id
       from ordered"
    )
  )
  
  srid = dbGetQuery(
    conn,
    glue(
      "select distinct 
       st_srid({rast}) as srid 
       from {layer}"))$srid %>% 
    st_crs
  
  boxes = dbGetQuery(
    conn, 
    glue(
      "select 
       st_asewkb(st_envelope({rast})) as geom 
       from {layer}
       order by rid asc"))$geom %>% 
    st_as_sfc(EWKB = T) 
  
  dims = dbGetQuery(
    conn, 
    glue(
      "select  
       st_pixelwidth({rast}) as dx, 
       st_pixelheight({rast}) as dy,
       st_skewx({rast}) as sx,
       st_skewy({rast}) as sy
       from {layer}
       limit 1"))
  
  result = map2(boxes, unique(values$id), function(x, y)
    st_as_stars(
      st_bbox(x),
      dx = dims$dx,
      dy = dims$dy,
      values = values[values$id == y, ]$values
    ) %>% st_set_crs(srid)) %>%
    do.call(what = st_mosaic)
  
  attr(attr(result, "dimensions"), "raster")$affine = c(dims[[3]],dims[[4]])
  
  result
}

pg_read_stars is a simplification of pgGetRast from rpostgis. It can only read one band and a spatial filter cannot be used to limit what is read, though the latter should not be too difficult to implement. For example, this is one possible way of using a spatial filter.

# bounding box of sf object/sfc converted to well-known text to be used as a filter
  ewkt = boundary %>%
      st_transform(crs) %>%
      st_bbox %>%
      st_as_sfc() %>%
      st_as_text(EWKT = T)
    
    values = dbGetQuery(
      conn,
      glue(
        "with clip as
          (select
           st_clip({raster_column},st_geomfromewkt('{ewkt}')) as clipped,
           rid
           from {layer})
         select unnest(st_dumpvalues(clipped,1)) as values,
         row_number() over (order by rid) as id
         from clip"
      )
    )
    
    boxes = dbGetQuery(
      conn,
      glue(
        "with clip as
            (select
             st_clip({raster_column},st_geomfromewkt('{ewkt}')) as clipped
             from {layer}
             where st_intersects({raster_column},st_geomfromewkt('{ewkt}')))
         select
         st_asbinary(st_envelope(clipped)) as geom
         from clip"
      )
    )$geom %>%
      st_as_sfc(crs = srid)

pg_read_stars works by first obtaining the values of the raster and assigning an ID based on which tile the value is from. The bounding boxes of the tiles that compose the raster are then read into R as well as the raster’s SRID and resolution. map2 from purrr is then used to make each bounding box/tile into a stars object inputted with the appropriate values and the resulting objects are then mosaicked to create the complete raster.

library(keyring)
library(mapview)

conn = DBI::dbConnect(
  RPostgres::Postgres(),
  dbname = key_list()[1, 1],
  user = key_list()[1, 2],
  password = key_get(key_list()[1, 1], key_list()[1, 2])
)

flood = pg_read_stars(conn,"floods")
mapview(flood, col.regions = "#226CE0", layer.name = "Flooding in Houston")
LS0tDQp0aXRsZTogInJlYWRpbmcgcG9zdGdpcyByYXN0ZXJzIg0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCkkgaGF2ZSBiZWVuIHVzaW5nIGEgZ3JlYXQgcGFja2FnZSBjYWxsZWQgW2BycG9zdGdpc2BdKGh0dHBzOi8vZ2l0aHViLmNvbS9tYWJsYWIvcnBvc3RnaXMpIGFzIGFuIGludGVyZmFjZSB0byBteSBQb3N0R0lTIGRhdGFiYXNlIHRvIHJlYWQgYW5kIHdyaXRlIHJhc3RlcnMuIEFzIG11Y2ggYXMgSSBlbmpveSB1c2luZyB0aGUgcGFja2FnZSwgSSBkbyBoYXZlIG9uZSByZXNlcnZhdGlvbiB3aXRoIGl0OiBpdHMgdXNlIG9mIGByYXN0ZXJgIGFuZCBwcm9qNHN0cmluZ3MuIFRvIGJldHRlciB1bmRlcnN0YW5kIGhvdyB0aGUgcGFja2FnZSB3b3JrcyBhbmQgdG8gZmluZCBhIHdheSB0byB1c2UgYHN0YXJzYCBvYmplY3RzIGluIHBsYWNlIG9mIGByYXN0ZXJgIGluIHRoZSByZWFkaW5nIGFuZCB3cml0aW5nIG9mIFBvc3RHSVMgcmFzdGVycywgSSB3ZW50IG9uIHRvIGZpbmQgYSB3YXkgdG8gcmVhZCBQb3N0R0lTIHJhc3RlcnMgaW50byBSIGFzIGBzdGFyc2Agb2JqZWN0cyB3aXRoIHRoZSBmdW5jdGlvbiAgYHBnX3JlYWRfc3RhcnNgLg0KIA0KYGBge3IgcGdfcmVhZF9zdGFyc30NCnBnX3JlYWRfc3RhcnMgPSBmdW5jdGlvbihjb25uLCBsYXllciwgcmFzdCA9ICJyYXN0Iil7DQogICMgcmVhZHMgcmFzdGVyIGZyb20gcG9zdGdpcyBkYXRhYmFzZQ0KICAjIGNvbm46IERCSUNvbm5lY3Rpb24gb2JqZWN0DQogICMgbGF5ZXI6IG5hbWUgb2YgdGFibGUgd2l0aCByYXN0ZXIgDQogICMgcmFzdDogbmFtZSBvZiBjb2x1bW4gdGhhdCBjb250YWlucyByYXN0ZXIgDQogIA0KICBsaWJyYXJ5KHN0YXJzKQ0KICBsaWJyYXJ5KHB1cnJyKQ0KICBsaWJyYXJ5KERCSSkNCiAgbGlicmFyeShnbHVlKQ0KICANCiAgdmFsdWVzID0gZGJHZXRRdWVyeSgNCiAgICBjb25uLA0KICAgIGdsdWUoDQogICAgICAid2l0aCBvcmRlcmVkIGFzICgNCiAgICAgICAgICBzZWxlY3QgDQogICAgICAgICAge3Jhc3R9IGFzIHJhc3QsIA0KICAgICAgICAgIHJpZCBmcm9tIHtsYXllcn0gDQogICAgICAgICAgb3JkZXIgYnkgcmlkIGFzYykNCiAgICAgICBzZWxlY3QNCiAgICAgICB1bm5lc3Qoc3RfZHVtcHZhbHVlcyhyYXN0LDEpKSBhcyB2YWx1ZXMsDQogICAgICAgcm93X251bWJlcigpIG92ZXIgKG9yZGVyIGJ5IHJpZCkgYXMgaWQNCiAgICAgICBmcm9tIG9yZGVyZWQiDQogICAgKQ0KICApDQogIA0KICBzcmlkID0gZGJHZXRRdWVyeSgNCiAgICBjb25uLA0KICAgIGdsdWUoDQogICAgICAic2VsZWN0IGRpc3RpbmN0IA0KICAgICAgIHN0X3NyaWQoe3Jhc3R9KSBhcyBzcmlkIA0KICAgICAgIGZyb20ge2xheWVyfSIpKSRzcmlkICU+JSANCiAgICBzdF9jcnMNCiAgDQogIGJveGVzID0gZGJHZXRRdWVyeSgNCiAgICBjb25uLCANCiAgICBnbHVlKA0KICAgICAgInNlbGVjdCANCiAgICAgICBzdF9hc2V3a2Ioc3RfZW52ZWxvcGUoe3Jhc3R9KSkgYXMgZ2VvbSANCiAgICAgICBmcm9tIHtsYXllcn0NCiAgICAgICBvcmRlciBieSByaWQgYXNjIikpJGdlb20gJT4lIA0KICAgIHN0X2FzX3NmYyhFV0tCID0gVCkgDQogIA0KICBkaW1zID0gZGJHZXRRdWVyeSgNCiAgICBjb25uLCANCiAgICBnbHVlKA0KICAgICAgInNlbGVjdCAgDQogICAgICAgc3RfcGl4ZWx3aWR0aCh7cmFzdH0pIGFzIGR4LCANCiAgICAgICBzdF9waXhlbGhlaWdodCh7cmFzdH0pIGFzIGR5LA0KICAgICAgIHN0X3NrZXd4KHtyYXN0fSkgYXMgc3gsDQogICAgICAgc3Rfc2tld3koe3Jhc3R9KSBhcyBzeQ0KICAgICAgIGZyb20ge2xheWVyfQ0KICAgICAgIGxpbWl0IDEiKSkNCiAgDQogIHJlc3VsdCA9IG1hcDIoYm94ZXMsIHVuaXF1ZSh2YWx1ZXMkaWQpLCBmdW5jdGlvbih4LCB5KQ0KICAgIHN0X2FzX3N0YXJzKA0KICAgICAgc3RfYmJveCh4KSwNCiAgICAgIGR4ID0gZGltcyRkeCwNCiAgICAgIGR5ID0gZGltcyRkeSwNCiAgICAgIHZhbHVlcyA9IHZhbHVlc1t2YWx1ZXMkaWQgPT0geSwgXSR2YWx1ZXMNCiAgICApICU+JSBzdF9zZXRfY3JzKHNyaWQpKSAlPiUNCiAgICBkby5jYWxsKHdoYXQgPSBzdF9tb3NhaWMpDQogIA0KICBhdHRyKGF0dHIocmVzdWx0LCAiZGltZW5zaW9ucyIpLCAicmFzdGVyIikkYWZmaW5lID0gYyhkaW1zW1szXV0sZGltc1tbNF1dKQ0KICANCiAgcmVzdWx0DQp9DQpgYGANCmBwZ19yZWFkX3N0YXJzYCBpcyBhIHNpbXBsaWZpY2F0aW9uIG9mIGBwZ0dldFJhc3RgIGZyb20gYHJwb3N0Z2lzYC4gSXQgY2FuIG9ubHkgcmVhZCBvbmUgYmFuZCBhbmQgYSBzcGF0aWFsIGZpbHRlciBjYW5ub3QgYmUgdXNlZCB0byBsaW1pdCB3aGF0IGlzIHJlYWQsIHRob3VnaCB0aGUgbGF0dGVyIHNob3VsZCBub3QgYmUgdG9vIGRpZmZpY3VsdCB0byBpbXBsZW1lbnQuIEZvciBleGFtcGxlLCB0aGlzIGlzIG9uZSBwb3NzaWJsZSB3YXkgb2YgdXNpbmcgYSBzcGF0aWFsIGZpbHRlci4gDQoNCmBgYHtyIHVzaW5nIGEgc3BhdGlhbCBmaWx0ZXIsIGV2YWwgPSBGfQ0KIyBib3VuZGluZyBib3ggb2Ygc2Ygb2JqZWN0L3NmYyBjb252ZXJ0ZWQgdG8gd2VsbC1rbm93biB0ZXh0IHRvIGJlIHVzZWQgYXMgYSBmaWx0ZXINCiAgZXdrdCA9IGJvdW5kYXJ5ICU+JQ0KICAgICAgc3RfdHJhbnNmb3JtKGNycykgJT4lDQogICAgICBzdF9iYm94ICU+JQ0KICAgICAgc3RfYXNfc2ZjKCkgJT4lDQogICAgICBzdF9hc190ZXh0KEVXS1QgPSBUKQ0KICAgIA0KICAgIHZhbHVlcyA9IGRiR2V0UXVlcnkoDQogICAgICBjb25uLA0KICAgICAgZ2x1ZSgNCiAgICAgICAgIndpdGggY2xpcCBhcw0KICAgICAgICAgIChzZWxlY3QNCiAgICAgICAgICAgc3RfY2xpcCh7cmFzdGVyX2NvbHVtbn0sc3RfZ2VvbWZyb21ld2t0KCd7ZXdrdH0nKSkgYXMgY2xpcHBlZCwNCiAgICAgICAgICAgcmlkDQogICAgICAgICAgIGZyb20ge2xheWVyfSkNCiAgICAgICAgIHNlbGVjdCB1bm5lc3Qoc3RfZHVtcHZhbHVlcyhjbGlwcGVkLDEpKSBhcyB2YWx1ZXMsDQogICAgICAgICByb3dfbnVtYmVyKCkgb3ZlciAob3JkZXIgYnkgcmlkKSBhcyBpZA0KICAgICAgICAgZnJvbSBjbGlwIg0KICAgICAgKQ0KICAgICkNCiAgICANCiAgICBib3hlcyA9IGRiR2V0UXVlcnkoDQogICAgICBjb25uLA0KICAgICAgZ2x1ZSgNCiAgICAgICAgIndpdGggY2xpcCBhcw0KICAgICAgICAgICAgKHNlbGVjdA0KICAgICAgICAgICAgIHN0X2NsaXAoe3Jhc3Rlcl9jb2x1bW59LHN0X2dlb21mcm9tZXdrdCgne2V3a3R9JykpIGFzIGNsaXBwZWQNCiAgICAgICAgICAgICBmcm9tIHtsYXllcn0NCiAgICAgICAgICAgICB3aGVyZSBzdF9pbnRlcnNlY3RzKHtyYXN0ZXJfY29sdW1ufSxzdF9nZW9tZnJvbWV3a3QoJ3tld2t0fScpKSkNCiAgICAgICAgIHNlbGVjdA0KICAgICAgICAgc3RfYXNiaW5hcnkoc3RfZW52ZWxvcGUoY2xpcHBlZCkpIGFzIGdlb20NCiAgICAgICAgIGZyb20gY2xpcCINCiAgICAgICkNCiAgICApJGdlb20gJT4lDQogICAgICBzdF9hc19zZmMoY3JzID0gc3JpZCkNCmBgYA0KDQpgcGdfcmVhZF9zdGFyc2Agd29ya3MgYnkgZmlyc3Qgb2J0YWluaW5nIHRoZSB2YWx1ZXMgb2YgdGhlIHJhc3RlciBhbmQgYXNzaWduaW5nIGFuIElEIGJhc2VkIG9uIHdoaWNoIHRpbGUgdGhlIHZhbHVlIGlzIGZyb20uIFRoZSBib3VuZGluZyBib3hlcyBvZiB0aGUgdGlsZXMgdGhhdCBjb21wb3NlIHRoZSByYXN0ZXIgYXJlIHRoZW4gcmVhZCBpbnRvIFIgYXMgd2VsbCBhcyB0aGUgcmFzdGVyJ3MgU1JJRCBhbmQgcmVzb2x1dGlvbi4gYG1hcDJgIGZyb20gYHB1cnJyYCBpcyB0aGVuIHVzZWQgdG8gbWFrZSBlYWNoIGJvdW5kaW5nIGJveC90aWxlIGludG8gYSBgc3RhcnNgIG9iamVjdCBpbnB1dHRlZCB3aXRoIHRoZSBhcHByb3ByaWF0ZSB2YWx1ZXMgYW5kIHRoZSByZXN1bHRpbmcgb2JqZWN0cyBhcmUgdGhlbiBtb3NhaWNrZWQgdG8gY3JlYXRlIHRoZSBjb21wbGV0ZSByYXN0ZXIuICANCg0KDQpgYGB7ciBmdW5jdGlvbiBleGFtcGxlLCBtZXNzYWdlPUYsIHdhcm5pbmc9RiwgZmlnLmhlaWdodD02LCBmaWcud2lkdGg9OX0NCmxpYnJhcnkoa2V5cmluZykNCmxpYnJhcnkobWFwdmlldykNCg0KY29ubiA9IERCSTo6ZGJDb25uZWN0KA0KICBSUG9zdGdyZXM6OlBvc3RncmVzKCksDQogIGRibmFtZSA9IGtleV9saXN0KClbMSwgMV0sDQogIHVzZXIgPSBrZXlfbGlzdCgpWzEsIDJdLA0KICBwYXNzd29yZCA9IGtleV9nZXQoa2V5X2xpc3QoKVsxLCAxXSwga2V5X2xpc3QoKVsxLCAyXSkNCikNCg0KZmxvb2QgPSBwZ19yZWFkX3N0YXJzKGNvbm4sImZsb29kcyIpDQptYXB2aWV3KGZsb29kLCBjb2wucmVnaW9ucyA9ICIjMjI2Q0UwIiwgbGF5ZXIubmFtZSA9ICJGbG9vZGluZyBpbiBIb3VzdG9uIikNCmBgYA==