Repository: geopandas/geopandas

Issue:
BUG: Uppercase table name will lead to psycopg2.errors.UndefinedTable when using to_postgis()
Here is my  pseudo code: 
`from sqlalchemy import create_engine`
`from osgeo import gdal`
`import geopandas as gpd`
`gdal.SetConfigOption("SHAPE_ENCODING", "GBK")`

`engine = create_engine(...)`
`shp = gpd.read_file(...)`
`shp.to_postgis(name='Countries', con=engine, if_exists='replace')`

And the error is

`psycopg2.errors.UndefinedTable: relation "countries" does not exist`
That's may because when creating the table, the name is "public"."Countries", but when inserting data, the name becomes public.Countries so it changes to public.countries automatically, and is different from the existing table.


Discussion:
Thanks for the report. I have to admit that I am not sure where this change happens, but I believe it is somewhere in `pandas.to_sql` we use here. Is the issue present with any table with a capital letter or there's some other pattern? Also, can you post the full traceback (full error message)?
> 
> 
> Thanks for the report. I have to admit that I am not sure where this change happens, but I believe it is somewhere in `pandas.to_sql` we use here. Is the issue present with any table with a capital letter or there's some other pattern? Also, can you post the full traceback (full error message)?

Sorry for forgetting this issue last day... And here are my findings:
①Yes, any capital letter will lead to errors
Here is the code:
```python
import geopandas as gpd
from shapely import geometry as geom
from sqlalchemy import create_engine

data = gpd.GeoDataFrame([geom.Point(110, 60), geom.Point(110.5, 50.4)],columns=['geometry'])
engine = create_engine('postgresql://postgres:postgres@localhost/postgres')
data.to_postgis(name='aPoint',con=engine,schema='test')
```
And the traceback is
```python
Traceback (most recent call last):
  File "D:/code_geass/py/main.py", line 7, in <module>
    data.to_postgis(name='aPoint',con=engine,schema='test')
  File "C:\software\miniconda3\envs\py\lib\site-packages\geopandas\geodataframe.py", line 1097, in to_postgis
    geopandas.io.sql._write_postgis(
  File "C:\software\miniconda3\envs\py\lib\site-packages\geopandas\io\sql.py", line 392, in _write_postgis
    gdf.to_sql(
  File "C:\software\miniconda3\envs\py\lib\site-packages\pandas\core\generic.py", line 2776, in to_sql
    sql.to_sql(
  File "C:\software\miniconda3\envs\py\lib\site-packages\pandas\io\sql.py", line 590, in to_sql
    pandas_sql.to_sql(
  File "C:\software\miniconda3\envs\py\lib\site-packages\pandas\io\sql.py", line 1397, in to_sql
    table.insert(chunksize, method=method)
  File "C:\software\miniconda3\envs\py\lib\site-packages\pandas\io\sql.py", line 831, in insert
    exec_insert(conn, keys, chunk_iter)
  File "C:\software\miniconda3\envs\py\lib\site-packages\geopandas\io\sql.py", line 273, in _psql_insert_copy
    cur.copy_expert(sql=sql, file=s_buf)
psycopg2.errors.UndefinedTable: relation "test.apoint" does not exist
```
②pandas.to_sql works well even there are capital letters. I tried to create a table 'aPoint' from a dataframe, and then I got it, whose name is 'aPoint' (right name, instead of apoint, which seems to be shown in the insert sql). The problem seems to be in the insert sql, but my ability is not enough to understand the source code
Thanks a lot for the details! We'll have to track where it happens because it does not seem to be directly caused by our code (we do not have any `lower` in there).

Resolving patch:
diff --git a/geopandas/io/sql.py b/geopandas/io/sql.py
--- a/geopandas/io/sql.py
+++ b/geopandas/io/sql.py
@@ -306,7 +306,9 @@ def _psql_insert_copy(tbl, conn, keys, data_iter):
 
     dbapi_conn = conn.connection
     with dbapi_conn.cursor() as cur:
-        sql = "COPY {} ({}) FROM STDIN WITH CSV".format(tbl.table.fullname, columns)
+        sql = 'COPY "{}"."{}" ({}) FROM STDIN WITH CSV'.format(
+            tbl.table.schema, tbl.table.name, columns
+        )
         cur.copy_expert(sql=sql, file=s_buf)
 
 
@@ -399,14 +401,14 @@ def _write_postgis(
     # Convert geometries to EWKB
     gdf = _convert_to_ewkb(gdf, geom_name, srid)
 
+    if schema is not None:
+        schema_name = schema
+    else:
+        schema_name = "public"
+
     if if_exists == "append":
         # Check that the geometry srid matches with the current GeoDataFrame
         with _get_conn(con) as connection:
-            if schema is not None:
-                schema_name = schema
-            else:
-                schema_name = "public"
-
             # Only check SRID if table exists
             if connection.dialect.has_table(connection, name, schema):
                 target_srid = connection.execute(
@@ -429,7 +431,7 @@ def _write_postgis(
         gdf.to_sql(
             name,
             connection,
-            schema=schema,
+            schema=schema_name,
             if_exists=if_exists,
             index=index,
             index_label=index_label,


Test patch:
diff --git a/geopandas/io/tests/test_sql.py b/geopandas/io/tests/test_sql.py
--- a/geopandas/io/tests/test_sql.py
+++ b/geopandas/io/tests/test_sql.py
@@ -344,6 +344,21 @@ def test_write_postgis_default(self, engine_postgis, df_nybb):
         df = read_postgis(sql, engine, geom_col="geometry")
         validate_boro_df(df)
 
+    def test_write_postgis_uppercase_tablename(self, engine_postgis, df_nybb):
+        """Tests writing GeoDataFrame to PostGIS with uppercase tablename."""
+        engine = engine_postgis
+        table = "aTestTable"
+
+        # If table exists, delete it before trying to write with defaults
+        drop_table_if_exists(engine, table)
+
+        # Write to db
+        write_postgis(df_nybb, con=engine, name=table, if_exists="fail")
+        # Validate
+        sql = 'SELECT * FROM "{table}";'.format(table=table)
+        df = read_postgis(sql, engine, geom_col="geometry")
+        validate_boro_df(df)
+
     def test_write_postgis_sqlalchemy_connection(self, engine_postgis, df_nybb):
         """Tests that GeoDataFrame can be written to PostGIS with defaults."""
         with engine_postgis.begin() as con: