Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport release/3.6] GPKG: add compatibility with GPKG 1.0 gpkg_data_column_constraints table #6627

Merged
merged 1 commit into from Nov 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 48 additions & 0 deletions autotest/ogr/ogr_gpkg.py
Expand Up @@ -6063,6 +6063,54 @@ def test_ogr_gpkg_field_domains_errors():
gdal.Unlink(filename)


###############################################################################
# Test gpkg_data_column_constraints of GPKG 1.0


def test_ogr_gpkg_field_domain_gpkg_1_0():

filename = "/vsimem/test.gpkg"

ds = gdal.GetDriverByName("GPKG").Create(
filename, 0, 0, 0, gdal.GDT_Unknown, options=["VERSION=1.0"]
)
ds.CreateLayer("test")
assert ds.AddFieldDomain(
ogr.CreateRangeFieldDomain(
"range_domain_int",
"my desc",
ogr.OFTReal,
ogr.OFSTNone,
1.5,
True,
2.5,
False,
)
)
ds = None

assert validate(filename)

ds = gdal.OpenEx(filename, gdal.OF_VECTOR)

gdal.ErrorReset()
domain = ds.GetFieldDomain("range_domain_int")
assert gdal.GetLastErrorMsg() == ""
assert domain is not None
assert domain.GetName() == "range_domain_int"
assert domain.GetDescription() == "my desc"
assert domain.GetDomainType() == ogr.OFDT_RANGE
assert domain.GetFieldType() == ogr.OFTReal
assert domain.GetMinAsDouble() == 1.5
assert domain.IsMinInclusive()
assert domain.GetMaxAsDouble() == 2.5
assert not domain.IsMaxInclusive()

ds = None

gdal.Unlink(filename)


###############################################################################
# Test attribute and spatial views

Expand Down
1 change: 1 addition & 0 deletions ogr/ogrsf_frmts/gpkg/ogr_geopackage.h
Expand Up @@ -335,6 +335,7 @@ class GDALGeoPackageDataset final : public OGRSQLiteBaseDataSource, public GDALG

bool HasDataColumnsTable() const;
bool HasDataColumnConstraintsTable() const;
bool HasDataColumnConstraintsTableGPKG_1_0() const;
bool CreateColumnsTableAndColumnConstraintsTablesIfNecessary();
bool HasGpkgextRelationsTable() const;
bool HasQGISLayerStyles() const;
Expand Down
67 changes: 54 additions & 13 deletions ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp
Expand Up @@ -3497,6 +3497,29 @@ bool GDALGeoPackageDataset::HasDataColumnConstraintsTable() const
return nCount == 1;
}

/************************************************************************/
/* HasDataColumnConstraintsTableGPKG_1_0() */
/************************************************************************/

bool GDALGeoPackageDataset::HasDataColumnConstraintsTableGPKG_1_0() const
{
if( m_nApplicationId != GP10_APPLICATION_ID )
return false;
// In GPKG 1.0, the columns were named minIsInclusive, maxIsInclusive
// They were changed in 1.1 to min_is_inclusive, max_is_inclusive
bool bRet = false;
sqlite3_stmt* hSQLStmt = nullptr;
int rc = sqlite3_prepare_v2( hDB,
"SELECT minIsInclusive, maxIsInclusive FROM gpkg_data_column_constraints", -1,
&hSQLStmt, nullptr );
if( rc == SQLITE_OK )
{
bRet = true;
sqlite3_finalize(hSQLStmt);
}
return bRet;
}

/************************************************************************/
/* CreateColumnsTableAndColumnConstraintsTablesIfNecessary() */
/************************************************************************/
Expand Down Expand Up @@ -3525,18 +3548,21 @@ bool GDALGeoPackageDataset::CreateColumnsTableAndColumnConstraintsTablesIfNecess
}
if( !HasDataColumnConstraintsTable() )
{
if( OGRERR_NONE != SQLCommand(GetDB(),
"CREATE TABLE gpkg_data_column_constraints ("
const char* min_is_inclusive = m_nApplicationId != GP10_APPLICATION_ID ? "min_is_inclusive": "minIsInclusive";
const char* max_is_inclusive = m_nApplicationId != GP10_APPLICATION_ID ? "max_is_inclusive": "maxIsInclusive";

const std::string osSQL(CPLSPrintf("CREATE TABLE gpkg_data_column_constraints ("
"constraint_name TEXT NOT NULL,"
"constraint_type TEXT NOT NULL,"
"value TEXT,"
"min NUMERIC,"
"min_is_inclusive BOOLEAN,"
"%s BOOLEAN,"
"max NUMERIC,"
"max_is_inclusive BOOLEAN,"
"%s BOOLEAN,"
"description TEXT,"
"CONSTRAINT gdcc_ntv UNIQUE (constraint_name, "
"constraint_type, value));") )
"constraint_type, value));", min_is_inclusive, max_is_inclusive));
if( OGRERR_NONE != SQLCommand(GetDB(), osSQL.c_str()) )
{
return false;
}
Expand Down Expand Up @@ -8045,21 +8071,26 @@ const OGRFieldDomain* GDALGeoPackageDataset::GetFieldDomain(const std::string& n
if( !HasDataColumnConstraintsTable() )
return nullptr;

const bool bIsGPKG10 = HasDataColumnConstraintsTableGPKG_1_0();
const char* min_is_inclusive = bIsGPKG10 ? "minIsInclusive" : "min_is_inclusive";
const char* max_is_inclusive = bIsGPKG10 ? "maxIsInclusive" : "max_is_inclusive";

std::unique_ptr<SQLResult> oResultTable;
// Note: for coded domains, we use a little trick by using a dummy
// _{domainname}_domain_description enum that has a single entry whose
// description is the description of the main domain.
{
char* pszSQL = sqlite3_mprintf(
"SELECT constraint_type, value, min, min_is_inclusive, "
"max, max_is_inclusive, description, constraint_name "
"SELECT constraint_type, value, min, %s, "
"max, %s, description, constraint_name "
"FROM gpkg_data_column_constraints "
"WHERE constraint_name IN ('%q', '_%q_domain_description') "
"AND length(constraint_type) < 100 " // to avoid denial of service
"AND (value IS NULL OR length(value) < 10000) " // to avoid denial of service
"AND (description IS NULL OR length(description) < 10000) " // to avoid denial of service
"ORDER BY value "
"LIMIT 10000", // to avoid denial of service
min_is_inclusive, max_is_inclusive,
name.c_str(), name.c_str());
oResultTable = SQLQuery(hDB, pszSQL);
sqlite3_free(pszSQL);
Expand Down Expand Up @@ -8347,6 +8378,10 @@ bool GDALGeoPackageDataset::AddFieldDomain(std::unique_ptr<OGRFieldDomain>&& dom
if( !CreateColumnsTableAndColumnConstraintsTablesIfNecessary() )
return false;

const bool bIsGPKG10 = HasDataColumnConstraintsTableGPKG_1_0();
const char* min_is_inclusive = bIsGPKG10 ? "minIsInclusive" : "min_is_inclusive";
const char* max_is_inclusive = bIsGPKG10 ? "maxIsInclusive" : "max_is_inclusive";

const auto& osDescription = domain->GetDescription();
switch( domain->GetDomainType() )
{
Expand All @@ -8363,10 +8398,12 @@ bool GDALGeoPackageDataset::AddFieldDomain(std::unique_ptr<OGRFieldDomain>&& dom
char* pszSQL = sqlite3_mprintf(
"INSERT INTO gpkg_data_column_constraints ("
"constraint_name, constraint_type, value, "
"min, min_is_inclusive, max, max_is_inclusive, "
"min, %s, max, %s, "
"description) VALUES ("
"'_%q_domain_description', 'enum', '', NULL, NULL, NULL, "
"NULL, %Q)",
min_is_inclusive,
max_is_inclusive,
domainName.c_str(),
osDescription.c_str());
CPL_IGNORE_RET_VAL(SQLCommand(hDB, pszSQL));
Expand All @@ -8378,9 +8415,11 @@ bool GDALGeoPackageDataset::AddFieldDomain(std::unique_ptr<OGRFieldDomain>&& dom
char* pszSQL = sqlite3_mprintf(
"INSERT INTO gpkg_data_column_constraints ("
"constraint_name, constraint_type, value, "
"min, min_is_inclusive, max, max_is_inclusive, "
"min, %s, max, %s, "
"description) VALUES ("
"'%q', 'enum', '%q', NULL, NULL, NULL, NULL, %Q)",
min_is_inclusive,
max_is_inclusive,
domainName.c_str(),
enumeration[i].pszCode,
enumeration[i].pszValue);
Expand Down Expand Up @@ -8435,11 +8474,11 @@ bool GDALGeoPackageDataset::AddFieldDomain(std::unique_ptr<OGRFieldDomain>&& dom
}

sqlite3_stmt* hInsertStmt = nullptr;
const char* pszSQL = "INSERT INTO gpkg_data_column_constraints ("
const char* pszSQL = CPLSPrintf("INSERT INTO gpkg_data_column_constraints ("
"constraint_name, constraint_type, value, "
"min, min_is_inclusive, max, max_is_inclusive, "
"min, %s, max, %s, "
"description) VALUES ("
"?, 'range', NULL, ?, ?, ?, ?, ?)";
"?, 'range', NULL, ?, ?, ?, ?, ?)", min_is_inclusive, max_is_inclusive);
if ( sqlite3_prepare_v2(hDB, pszSQL, -1, &hInsertStmt, nullptr)
!= SQLITE_OK )
{
Expand Down Expand Up @@ -8482,9 +8521,11 @@ bool GDALGeoPackageDataset::AddFieldDomain(std::unique_ptr<OGRFieldDomain>&& dom
char* pszSQL = sqlite3_mprintf(
"INSERT INTO gpkg_data_column_constraints ("
"constraint_name, constraint_type, value, "
"min, min_is_inclusive, max, max_is_inclusive, "
"min, %s, max, %s, "
"description) VALUES ("
"'%q', 'glob', '%q', NULL, NULL, NULL, NULL, %Q)",
min_is_inclusive,
max_is_inclusive,
domainName.c_str(),
poGlobDomain->GetGlob().c_str(),
osDescription.empty() ? nullptr : osDescription.c_str());
Expand Down
52 changes: 36 additions & 16 deletions swig/python/gdal-utils/osgeo_utils/samples/validate_gpkg.py
Expand Up @@ -764,6 +764,7 @@ def _check_vector_user_table(self, c, table_name):
"SELECT %s FROM %s " % (_esc_id(geom_column_name), _esc_id(table_name))
)
found_geom_types = set()
warning_messages = set()
for (blob,) in c.fetchall():
if blob is None:
continue
Expand All @@ -788,15 +789,14 @@ def _check_vector_user_table(self, c, table_name):
)
endian_prefix = ">" if big_endian else "<"
geom_srs_id = struct.unpack((endian_prefix + "I") * 1, blob[4:8])[0]
self._assert(
srs_id == geom_srs_id,
33,
(
if srs_id != geom_srs_id:
warning_msg = (
"table %s has geometries with SRID %d, "
+ "whereas only %d is expected"
)
% (table_name, geom_srs_id, srs_id),
)
) % (table_name, geom_srs_id, srs_id)
if warning_msg not in warning_messages:
warning_messages.add(warning_msg)
self._assert(False, 33, warning_msg)

self._assert(
not (empty_flag and env_ind != 0), 152, "Invalid empty geometry"
Expand Down Expand Up @@ -878,8 +878,8 @@ def _check_vector_user_table(self, c, table_name):
self._assert(
not found_geom_types or found_geom_types == set([geometry_type_name]),
32,
"in table %s, found geometry types %s"
% (table_name, str(found_geom_types)),
"in table %s, found geometry types %s whereas %s was expected"
% (table_name, str(found_geom_types), geometry_type_name),
)
elif geometry_type_name == "GEOMETRYCOLLECTION":
self._assert(
Expand Down Expand Up @@ -2006,6 +2006,8 @@ def _check_gpkg_extensions(self, c):
]
for geom_name in GPKGChecker.EXT_GEOM_TYPES:
KNOWN_EXTENSIONS += ["gpkg_geom_" + geom_name]
if self.version < (1, 2):
KNOWN_EXTENSIONS += ["gpkg_geometry_type_trigger", "gpkg_srs_id_trigger"]

for (extension_name,) in rows:

Expand Down Expand Up @@ -2440,14 +2442,24 @@ def _check_schema(self, c):

c.execute("PRAGMA table_info(gpkg_data_column_constraints)")
columns = c.fetchall()

# GPKG 1.1 uses min_is_inclusive/max_is_inclusive but GPKG 1.0 had
# minIsInclusive/maxIsInclusive
min_is_inclusive = (
"min_is_inclusive" if self.version >= (1, 1) else "minIsInclusive"
)
max_is_inclusive = (
"max_is_inclusive" if self.version >= (1, 1) else "maxIsInclusive"
)

expected_columns = [
(0, "constraint_name", "TEXT", 1, None, 0),
(1, "constraint_type", "TEXT", 1, None, 0),
(2, "value", "TEXT", 0, None, 0),
(3, "min", "NUMERIC", 0, None, 0),
(4, "min_is_inclusive", "BOOLEAN", 0, None, 0),
(4, min_is_inclusive, "BOOLEAN", 0, None, 0),
(5, "max", "NUMERIC", 0, None, 0),
(6, "max_is_inclusive", "BOOLEAN", 0, None, 0),
(6, max_is_inclusive, "BOOLEAN", 0, None, 0),
(7, "description", "TEXT", 0, None, 0),
]
self._check_structure(
Expand Down Expand Up @@ -2531,7 +2543,7 @@ def _check_schema(self, c):

c.execute(
"SELECT 1 FROM gpkg_data_column_constraints WHERE "
+ "constraint_type = 'range' AND min_is_inclusive NOT IN (0,1)"
+ f"constraint_type = 'range' AND {min_is_inclusive} NOT IN (0,1)"
)
if c.fetchone() is not None:
self._assert(
Expand All @@ -2544,7 +2556,7 @@ def _check_schema(self, c):

c.execute(
"SELECT 1 FROM gpkg_data_column_constraints WHERE "
+ "constraint_type = 'range' AND max_is_inclusive NOT IN (0,1)"
+ f"constraint_type = 'range' AND {max_is_inclusive} NOT IN (0,1)"
)
if c.fetchone() is not None:
self._assert(
Expand All @@ -2555,7 +2567,7 @@ def _check_schema(self, c):
+ "not 0 or 1",
)

for col_name in ("min", "min_is_inclusive", "max", "max_is_inclusive"):
for col_name in ("min", min_is_inclusive, "max", max_is_inclusive):
c.execute(
"SELECT 1 FROM gpkg_data_column_constraints WHERE "
+ "constraint_type IN ('enum', 'glob') AND "
Expand Down Expand Up @@ -2742,8 +2754,11 @@ def check(self):
("Wrong application_id: %s. " + "Expected one of GP10, GP11, GPKG")
% str(application_id),
)

if application_id == gpkg:
if application_id == gp10:
self.version = (1, 0)
elif application_id == gp11:
self.version = (1, 1)
elif application_id == gpkg:
f.seek(60, 0)
user_version = f.read(4)
expected_version = 10200
Expand All @@ -2754,6 +2769,11 @@ def check(self):
"Wrong user_version: %d. Expected >= %d"
% (user_version, expected_version),
)
self.version = (
expected_version // 10000,
(expected_version % 10000) // 100,
expected_version % 100,
)

conn = sqlite3.connect(":memory:")
c = conn.cursor()
Expand Down