当多行 INSERT 由于几何类型不匹配而失败时,我可以让 PostgreSQL 报告有问题的行吗?

das*_*s-g 5 postgresql insert error-handling postgis

INSERT INTO当其中一些行的 PostGIS 几何类型的值与目标表中相应行的 PostGIS 几何类型不兼容时,使用单个语句将多行插入到表中会失败(如预期的那样):

CREATE EXTENSION postgis;

CREATE TABLE t (
    id integer,
    p geometry(POINT)
);

INSERT INTO t
VALUES
    ( 1, ST_GeometryFromText('Point(0 0)')      ),
    ( 2, ST_GeometryFromText('Point(1 2)')      ),
    ( 3, ST_GeometryFromText('MultiPoint(2 3)') ),
    ( 4, ST_GeometryFromText('Point(5 23)')     ),
    ( 5, ST_GeometryFromText('Point(42 36)')    );
Run Code Online (Sandbox Code Playgroud)

错误消息告诉我们到底出了什么问题:

CREATE EXTENSION postgis;

CREATE TABLE t (
    id integer,
    p geometry(POINT)
);

INSERT INTO t
VALUES
    ( 1, ST_GeometryFromText('Point(0 0)')      ),
    ( 2, ST_GeometryFromText('Point(1 2)')      ),
    ( 3, ST_GeometryFromText('MultiPoint(2 3)') ),
    ( 4, ST_GeometryFromText('Point(5 23)')     ),
    ( 5, ST_GeometryFromText('Point(42 36)')    );
Run Code Online (Sandbox Code Playgroud)

但它缺乏有用的上下文信息,例如:

  • 有多少行INSERT有这个问题?
  • 确切的数值是多少?
  • 违规行的其他列中的值是什么?(在上面的示例中:违规行的 ID 是什么?)

我可以修改 INSERT 语句,以便 PostgreSQL 给我这些信息,例如,在错误消息中包含违规行的完整内容,就像违反NOT NULL约束一样?

例如

CREATE TABLE s (
   i integer NOT NULL, t text
);

INSERT INTO s
VALUES
    (1   , 'foo'),
    (NULL, 'bar'),
    (2   , 'baz');
Run Code Online (Sandbox Code Playgroud)

产生更有用的消息:

ERROR:  Geometry type (MultiPoint) does not match column type (Point)
Run Code Online (Sandbox Code Playgroud)

动机/用例

当您可以查看语句中VALUES列出的内容INSERT并查看违规行时,这当然不是那么重要。但是,当插入的行是从另一个表中选择或动态计算时,会出现同样的问题,然后提供更多信息的错误消息确实很有用。

joa*_*olo 3

解决方法(因为我不认为自己这是“答案”)

您可以使用一个函数循环遍历您尝试插入到最终表中的选择的结果,并在插入值之前通知您。如果其中一个失败,您知道是哪一个:

您创建一个函数:

CREATE OR REPLACE FUNCTION insert_one_by_one (_sql_select_txt text) RETURNS void AS
$$
DECLARE
    _r t%rowtype ;
BEGIN
    FOR _r IN EXECUTE _sql_select_txt LOOP
        RAISE NOTICE 'Going to insert: id=%, p=%', _r.id, ST_AsText(_r.p) ;
        INSERT INTO t VALUES (_r.*) ;
    END LOOP ;
END ;
$$
LANGUAGE plpgsql ;
Run Code Online (Sandbox Code Playgroud)

您可以通过传递一个(格式良好的)SQL 语句的文本来调用它,该语句返回一个与以下类型相同的表t

SELECT insert_one_by_one
($$
  SELECT * FROM
  (
  VALUES
      ( 1, ST_GeometryFromText('Point(0 0)')      ),
      ( 2, ST_GeometryFromText('Point(1 2)')      ),
      ( 3, ST_GeometryFromText('MultiPoint(2 3)') ),
      ( 4, ST_GeometryFromText('Point(5 23)')     ),
      ( 5, ST_GeometryFromText('Point(42 36)')    )
  ) AS v (id, p)
$$) ;
Run Code Online (Sandbox Code Playgroud)

这将产生以下输出(在 pgAdmin III 消息窗口上,或者在stdout/ stderr[我认为] 上,如果您使用psql):

NOTICE:  Going to insert: id=1, p=POINT(0 0)
CONTEXT:  PL/pgSQL function insert_one_by_one(text) line 6 at RAISE
NOTICE:  Going to insert: id=2, p=POINT(1 2)
CONTEXT:  PL/pgSQL function insert_one_by_one(text) line 6 at RAISE

ERROR:  Geometry type (MultiPoint) does not match column type (Point)
CONTEXT:  PL/pgSQL function insert_one_by_one(text) line 5 at FOR over EXECUTE statement

********** Error **********

ERROR: Geometry type (MultiPoint) does not match column type (Point)
SQL state: 22023
Context: PL/pgSQL function insert_one_by_one(text) line 5 at FOR over EXECUTE statement
Run Code Online (Sandbox Code Playgroud)

警告:这会严重减慢您的查询速度。恕我直言,仅当原始查询失败时才应将其用作“事后”调试工具INSERT


奇怪的是,我无法让函数在出现错误时通过捕获错误来写入消息。最初的目的是使用这个功能:

CREATE OR REPLACE FUNCTION insert_one_by_one (_sql_txt text) RETURNS void AS
$$
DECLARE
    _r t%rowtype ;
BEGIN
    FOR _r IN EXECUTE _sql_txt LOOP
        BEGIN
            INSERT INTO t VALUES (_r.*) ;
        EXCEPTION 
            WHEN SQLSTATE '22023' THEN
                RAISE EXCEPTION  'Error while trying to insert id = %, p = %', _r.id, _r.p USING ERRCODE = '22023';
            WHEN others THEN
                RAISE EXCEPTION  'What ??' ;
        END ;
    END LOOP ;
END ;
$$
LANGUAGE plpgsql ;
Run Code Online (Sandbox Code Playgroud)

...但是有了它,你就会得到:

********** Error **********

ERROR: Geometry type (MultiPoint) does not match column type (Point)
SQL state: 22023
Context: PL/pgSQL function insert_one_by_one(text) line 5 at FOR over EXECUTE statement
Run Code Online (Sandbox Code Playgroud)

我想,这意味着 PostGIS 在 PostgreSQL 内部进行了相当深入的攻击。