GeoPackage转达梦SQL工具 - 在线GPKG到达梦空间数据库建表脚本转换器

GeoPackage → 达梦 SQL 转换工具

上传 GeoPackage (.gpkg) 文件,自动生成达梦空间数据库的建表 SQL 和数据插入 SQL。如果需要把 Shapefile 或 gdb 转换为达梦 SQL,可以使用 QGIS 先把 Shapefile 或 gdb 转换为 GeoPackage。

使用说明
支持上传 GeoPackage (.gpkg) 文件,自动解析所有业务表结构和空间数据,生成达梦数据库建表脚本和 INSERT 语句。几何列使用 ST_GeomFromText 函数插入 WKT 格式数据。所有操作在浏览器本地完成,数据不会上传到服务器。该工具已经过GIS专业人员验证,请放心使用。

上传 GeoPackage 文件

点击或拖拽文件到此处上传

支持 .gpkg 格式(GeoPackage 空间数据库文件)

重要说明

  1. 达梦数据库安装包默认不含空间扩展,如需使用空间数据库功能,需要先安装空间包,详情参考:达梦数据库libgeos_c.dll加载失败解决方法:空间数据包安装指南
  2. 达梦数据库适配 GeoServer 使用的是 GEO2 引擎,执行 SQL 脚本前需要先执行 SP_INIT_GEO2_SYS(1)。详情参考:GeoServer适配达梦数据库完整教程:从账号创建到图层发布
  3. 本工具生成的创建数据表脚本 00_create_tables.sql 包含删除同名旧数据表语句,如需保留旧数据,请手动备份旧数据表。

格式说明

GeoPackage 是一种基于 SQLite 的开放地理空间数据格式,由 OGC(开放地理空间信息联盟)制定标准。 它以单个 .gpkg 文件存储矢量要素、栅格影像和元数据,广泛用于移动端和 Web GIS 应用。

达梦数据库(DM) 是国产关系型数据库管理系统,支持空间数据库扩展(SYSGEO2), 提供 ST_GEOMETRY 类型和 ST_GeomFromText 等空间函数。

转换特性

  • 自动解析 GeoPackage 中所有业务表(排除 gpkg_*、sqlite_*、rtree_* 系统表)
  • 支持点、线、面、多点、多线、多面等几何类型
  • 支持 2D 和 3D(Z/M/ZM)坐标数据
  • 几何列自动映射为 SYSGEO2.ST_GEOMETRY 类型
  • 数据插入使用 SYSGEO2.DMGEO2.ST_GeomFromText 函数
  • SQLite 类型自动映射为达梦数据库类型
  • 支持 SQL LIKE 模式过滤表名

类型映射

SQLite/GeoPackage 类型达梦数据库类型
INTEGERINTEGER
TEXT(n)VARCHAR2(n)
TEXTVARCHAR2(4000)
REALDOUBLE
MEDIUMINTINTEGER
DATETIMETIMESTAMP
BLOBBLOB
几何列SYSGEO2.ST_GEOMETRY

注意事项

  • 所有操作在浏览器本地完成,数据不会上传到服务器
  • 大文件(超过 100MB)可能导致浏览器卡顿,建议耐心等待
  • 建表 SQL 使用 DROP TABLE IF EXISTS 确保可重复执行
  • 生成的 SQL 包含 COMMIT 语句,确保数据持久化
  • 坐标系 SRID 需与达梦数据库中注册的坐标系一致

代码实现

JavaScript

/**
 * GeoPackage 转达梦 SQL
 * 需要安装: npm install sql.js
 * sql.js 的 WASM 文件需要放在 public 目录下
 */
import initSqlJs from 'sql.js';

/**
 * SQLite 类型 -> 达梦类型映射
 */
const TYPE_MAP = {
  'INTEGER': 'INTEGER', 'TEXT': 'VARCHAR2', 'REAL': 'DOUBLE',
  'MEDIUMINT': 'INTEGER', 'DATETIME': 'TIMESTAMP', 'BLOB': 'BLOB',
};

/**
 * 将 GeoPackage 二进制几何数据转换为 WKT
 */
function gpkgGeomToWkt(blob) {
  if (!blob || blob.length < 8) return null;
  if (blob[0] !== 0x47 || blob[1] !== 0x50) return wkbToWkt(blob);
  const flags = blob[3];
  const envelopeType = (flags >> 1) & 0x07;
  const emptyFlag = (flags >> 4) & 0x01;
  if (emptyFlag) return null;
  const envelopeSizes = { 0: 0, 1: 32, 2: 48, 3: 48, 4: 64 };
  const wkbOffset = 8 + (envelopeSizes[envelopeType] || 0);
  return wkbToWkt(blob.slice(wkbOffset));
}

function wkbToWkt(wkb) {
  const dv = new DataView(wkb.buffer, wkb.byteOffset, wkb.byteLength);
  return parseWkb(dv, 0)[0];
}

function parseWkb(dv, offset) {
  const le = dv.getUint8(offset) === 1;
  offset += 1;
  const fullType = dv.getUint32(offset, le);
  offset += 4;
  const baseType = fullType % 1000;
  const hasZ = Math.floor(fullType / 1000) === 1 || Math.floor(fullType / 1000) === 3;
  const hasM = Math.floor(fullType / 1000) === 2 || Math.floor(fullType / 1000) === 3;
  const dims = 2 + (hasZ ? 1 : 0) + (hasM ? 1 : 0);
  const dim = hasZ && hasM ? ' ZM' : hasZ ? ' Z' : hasM ? ' M' : '';

  const readCoord = (off) => {
    const parts = [];
    for (let i = 0; i < dims; i++) {
      parts.push(dv.getFloat64(off, le)); off += 8;
    }
    return [parts.join(' '), off];
  };

  if (baseType === 1) { // Point
    const [c, o] = readCoord(offset);
    return ['POINT' + dim + ' (' + c + ')', o];
  } else if (baseType === 2) { // LineString
    const n = dv.getUint32(offset, le); offset += 4;
    const coords = [];
    for (let i = 0; i < n; i++) { const [c, o] = readCoord(offset); offset = o; coords.push(c); }
    return ['LINESTRING' + dim + ' (' + coords.join(', ') + ')', offset];
  } else if (baseType === 3) { // Polygon
    const nRings = dv.getUint32(offset, le); offset += 4;
    const rings = [];
    for (let i = 0; i < nRings; i++) {
      const n = dv.getUint32(offset, le); offset += 4;
      const coords = [];
      for (let j = 0; j < n; j++) { const [c, o] = readCoord(offset); offset = o; coords.push(c); }
      rings.push('(' + coords.join(', ') + ')');
    }
    return ['POLYGON' + dim + ' (' + rings.join(', ') + ')', offset];
  }
  // Multi* 类型类似...
  throw new Error('Unsupported type: ' + fullType);
}

/**
 * 主转换函数
 * @param {File} file - GeoPackage 文件
 * @param {number} srid - 坐标系 SRID
 * @returns {Promise<{createSql: string, insertSqls: Object}>}
 */
async function convertGpkgToDmSql(file, srid = 3857) {
  const SQL = await initSqlJs({ locateFile: f => '/' + f });
  const buffer = await file.arrayBuffer();
  const db = new SQL.Database(new Uint8Array(buffer));

  // 获取所有业务表
  const tables = db.exec(
    "SELECT name FROM sqlite_master WHERE type='table' " +
    "AND name NOT LIKE 'gpkg_%' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'rtree_%' " +
    "ORDER BY name"
  )[0].values.map(r => r[0]);

  // 获取几何列信息
  const geomMap = {};
  const geomResult = db.exec('SELECT table_name, column_name FROM gpkg_geometry_columns');
  if (geomResult.length) {
    geomResult[0].values.forEach(r => { geomMap[r[0]] = r[1]; });
  }

  const createSqls = [];
  const insertSqls = {};

  for (const tableName of tables) {
    const columns = db.exec('PRAGMA table_info("' + tableName + '")')[0].values;
    const geomCol = geomMap[tableName];

    // 生成建表 SQL
    const colDefs = columns.map(col => {
      const [cid, name, type, notnull, dflt, pk] = col;
      let dmType = name === geomCol ? 'SYSGEO2.ST_GEOMETRY' : TYPE_MAP[type] || 'VARCHAR2(255)';
      let def = '    "' + name + '" ' + dmType;
      if (pk) def += ' PRIMARY KEY';
      if (notnull && !pk) def += ' NOT NULL';
      return def;
    });
    createSqls.push('DROP TABLE IF EXISTS "' + tableName + '";\n' +
      'CREATE TABLE "' + tableName + '" (\n' + colDefs.join(',\n') + '\n);');

    // 生成插入 SQL
    const count = db.exec('SELECT COUNT(*) FROM "' + tableName + '"')[0].values[0][0];
    if (count > 0) {
      const data = db.exec('SELECT * FROM "' + tableName + '"')[0];
      const geomIdx = columns.findIndex(c => c[1] === geomCol);
      const lines = [];
      for (const row of data.values) {
        const vals = row.map((val, i) => {
          if (i === geomIdx && val instanceof Uint8Array) {
            const wkt = gpkgGeomToWkt(val);
            return wkt ? 'SYSGEO2.DMGEO2.ST_GeomFromText(\'' + wkt + '\', ' + srid + ')' : 'NULL';
          }
          if (val === null) return 'NULL';
          if (typeof val === 'number') return String(val);
          return "'" + String(val).replace(/'/g, "''") + "'";
        });
        lines.push('INSERT INTO "' + tableName + '" VALUES (' + vals.join(', ') + ');');
      }
      insertSqls[tableName] = lines.join('\n');
    }
  }

  db.close();
  return { createSql: createSqls.join('\n\n'), insertSqls };
}

Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
从 GeoPackage 文件读取所有表,生成达梦数据库的建表 SQL 和插入数据 SQL。
几何列使用达梦空间数据库的 ST_GeomFromText 函数插入 WKT 格式数据。
"""

import sqlite3
import struct
import os

GPKG_PATH = r'./input.gpkg'
OUTPUT_DIR = r'./dm_sql'
SRID = 3857

# SQLite 类型 -> 达梦类型映射
TYPE_MAP = {
    'INTEGER': 'INTEGER', 'TEXT': 'VARCHAR2', 'REAL': 'DOUBLE',
    'MEDIUMINT': 'INTEGER', 'DATETIME': 'TIMESTAMP', 'BLOB': 'BLOB',
}

def parse_text_length(col_type):
    """从 TEXT(255) 中提取长度"""
    if col_type.startswith('TEXT'):
        if '(' in col_type:
            return int(col_type.split('(')[1].rstrip(')'))
        return 4000
    return None

def map_dm_type(col_name, col_type, geom_col_name=None):
    """将 SQLite 列类型映射为达梦类型"""
    upper_type = col_type.upper().strip()
    if geom_col_name and col_name == geom_col_name:
        return 'SYSGEO2.ST_GEOMETRY'
    if upper_type.startswith('TEXT'):
        length = parse_text_length(upper_type)
        return f'VARCHAR2({length})' if length else 'VARCHAR2(4000)'
    base_type = upper_type.split('(')[0].strip()
    return TYPE_MAP.get(base_type, 'VARCHAR2(255)')

def gpkg_geom_to_wkt(blob):
    """将 GeoPackage 二进制几何数据转换为 WKT 格式"""
    if blob is None or len(blob) < 8:
        return None
    if blob[0:2] != b'GP':
        return None
    flags = blob[3]
    envelope_type = (flags >> 1) & 0x07
    if (flags >> 4) & 0x01:
        return None
    envelope_sizes = {0: 0, 1: 32, 2: 48, 3: 48, 4: 64}
    wkb_offset = 8 + envelope_sizes.get(envelope_type, 0)
    return wkb_to_wkt(blob[wkb_offset:])

def wkb_to_wkt(wkb):
    """将 WKB 二进制数据转换为 WKT 文本"""
    if not wkb or len(wkb) < 5:
        return None
    try:
        return parse_wkb_geometry(wkb, 0)[0]
    except Exception:
        return None

def read_double(data, offset, bo):
    fmt = '<d' if bo == 1 else '>d'
    return struct.unpack_from(fmt, data, offset)[0], offset + 8

def read_uint32(data, offset, bo):
    fmt = '<I' if bo == 1 else '>I'
    return struct.unpack_from(fmt, data, offset)[0], offset + 4

def parse_wkb_geometry(data, offset):
    """递归解析 WKB 几何体"""
    bo = data[offset]
    offset += 1
    geom_type, offset = read_uint32(data, offset, bo)
    if geom_type == 1:  # Point
        x, offset = read_double(data, offset, bo)
        y, offset = read_double(data, offset, bo)
        return f'POINT ({x} {y})', offset
    elif geom_type == 2:  # LineString
        n, offset = read_uint32(data, offset, bo)
        coords = []
        for _ in range(n):
            x, offset = read_double(data, offset, bo)
            y, offset = read_double(data, offset, bo)
            coords.append(f'{x} {y}')
        return f'LINESTRING ({", ".join(coords)})', offset
    elif geom_type == 3:  # Polygon
        n_rings, offset = read_uint32(data, offset, bo)
        rings = []
        for _ in range(n_rings):
            n, offset = read_uint32(data, offset, bo)
            coords = []
            for _ in range(n):
                x, offset = read_double(data, offset, bo)
                y, offset = read_double(data, offset, bo)
                coords.append(f'{x} {y}')
            rings.append(f'({", ".join(coords)})')
        return f'POLYGON ({", ".join(rings)})', offset
    elif geom_type == 4:  # MultiPoint
        n, offset = read_uint32(data, offset, bo)
        points = []
        for _ in range(n):
            wkt, offset = parse_wkb_geometry(data, offset)
            points.append(wkt.replace('POINT ', ''))
        return f'MULTIPOINT ({", ".join(points)})', offset
    elif geom_type == 5:  # MultiLineString
        n, offset = read_uint32(data, offset, bo)
        lines = []
        for _ in range(n):
            wkt, offset = parse_wkb_geometry(data, offset)
            lines.append(wkt.replace('LINESTRING ', ''))
        return f'MULTILINESTRING ({", ".join(lines)})', offset
    elif geom_type == 6:  # MultiPolygon
        n, offset = read_uint32(data, offset, bo)
        polys = []
        for _ in range(n):
            wkt, offset = parse_wkb_geometry(data, offset)
            polys.append(wkt.replace('POLYGON ', ''))
        return f'MULTIPOLYGON ({", ".join(polys)})', offset
    else:
        raise ValueError(f'Unsupported geometry type: {geom_type}')

def escape_sql_value(val):
    """转义 SQL 字符串值"""
    if val is None:
        return 'NULL'
    if isinstance(val, float):
        # 浮点数无小数部分时显示为整数,与 JavaScript 行为一致
        if val == int(val) and abs(val) < 1e15:
            return str(int(val))
        return str(val)
    if isinstance(val, int):
        return str(val)
    if isinstance(val, bytes):
        return 'NULL'
    return "'" + str(val).replace("'", "''") + "'"

def main():
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    conn = sqlite3.connect(GPKG_PATH)
    cursor = conn.cursor()

    # 获取所有业务表
    cursor.execute(
        "SELECT name FROM sqlite_master WHERE type='table' "
        "AND name NOT LIKE 'gpkg_%' AND name NOT LIKE 'sqlite_%' "
        "AND name NOT LIKE 'rtree_%' ORDER BY name"
    )
    tables = [t[0] for t in cursor.fetchall()]

    # 获取几何列信息
    geom_map = {}
    cursor.execute('SELECT table_name, column_name FROM gpkg_geometry_columns')
    for row in cursor.fetchall():
        geom_map[row[0]] = row[1]

    all_create_sqls = [f'-- GeoPackage -> 达梦空间数据库\n-- 坐标系: EPSG:{SRID}\n']

    for table_name in tables:
        cursor.execute(f'PRAGMA table_info("{table_name}")')
        columns = cursor.fetchall()
        geom_col = geom_map.get(table_name)

        # 生成建表 SQL
        col_defs = []
        for col in columns:
            cid, name, col_type, notnull, default, pk = col
            dm_type = map_dm_type(name, col_type, geom_col)
            defn = f'    "{name}" {dm_type}'
            if pk: defn += ' PRIMARY KEY'
            if notnull and not pk: defn += ' NOT NULL'
            col_defs.append(defn)
        create_sql = f'DROP TABLE IF EXISTS "{table_name}";\nCREATE TABLE "{table_name}" (\n{",".join(col_defs)}\n);'
        all_create_sqls.append(create_sql)

        # 生成插入 SQL
        cursor.execute(f'SELECT COUNT(*) FROM "{table_name}"')
        count = cursor.fetchone()[0]
        if count > 0:
            geom_idx = next((i for i, c in enumerate(columns) if c[1] == geom_col), -1)
            cursor.execute(f'SELECT * FROM "{table_name}"')
            for row in cursor.fetchall():
                values = []
                for i, val in enumerate(row):
                    if i == geom_idx and val is not None:
                        wkt = gpkg_geom_to_wkt(val)
                        values.append(f"SYSGEO2.DMGEO2.ST_GeomFromText('{wkt}', {SRID})" if wkt else 'NULL')
                    else:
                        values.append(escape_sql_value(val))
                col_list = ', '.join(f'"{c[1]}"' for c in columns)
                print(f'INSERT INTO "{table_name}" ({col_list}) VALUES ({", ".join(values)});')

    # 写入建表 SQL 文件
    with open(os.path.join(OUTPUT_DIR, '00_create_tables.sql'), 'w', encoding='utf-8') as f:
        f.write('\n\n'.join(all_create_sqls))

    conn.close()
    print('完成!')

if __name__ == '__main__':
    main()

评论 (0)

登录 后发表评论

暂无评论,快来发表第一条评论吧!