243 lines
8.4 KiB
JavaScript
243 lines
8.4 KiB
JavaScript
const Database = require("better-sqlite3");
|
|
const path = require("path");
|
|
|
|
const dbPath = path.join(__dirname, "..", "data", "polygons.db");
|
|
const db = new Database(dbPath);
|
|
db.pragma("foreign_keys = ON");
|
|
|
|
db.prepare(`
|
|
CREATE TABLE IF NOT EXISTS geometries (
|
|
id TEXT PRIMARY KEY,
|
|
type TEXT,
|
|
is_deleted INTEGER NOT NULL DEFAULT 0,
|
|
draw_geometry TEXT NOT NULL,
|
|
binding TEXT,
|
|
time_start INTEGER,
|
|
time_end INTEGER,
|
|
bbox_min_lng REAL,
|
|
bbox_min_lat REAL,
|
|
bbox_max_lng REAL,
|
|
bbox_max_lat REAL,
|
|
created_at TEXT,
|
|
updated_at TEXT
|
|
)
|
|
`).run();
|
|
|
|
db.prepare(`
|
|
CREATE TABLE IF NOT EXISTS entities (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
slug TEXT UNIQUE,
|
|
description TEXT,
|
|
type_id TEXT NOT NULL DEFAULT 'country',
|
|
kind TEXT,
|
|
status INTEGER DEFAULT 1,
|
|
is_deleted INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT,
|
|
updated_at TEXT
|
|
)
|
|
`).run();
|
|
|
|
db.prepare(`
|
|
CREATE TABLE IF NOT EXISTS entity_geometries (
|
|
entity_id TEXT NOT NULL,
|
|
geometry_id TEXT NOT NULL,
|
|
created_at TEXT,
|
|
PRIMARY KEY (entity_id, geometry_id),
|
|
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (geometry_id) REFERENCES geometries(id) ON DELETE CASCADE
|
|
)
|
|
`).run();
|
|
|
|
ensureColumn("entities", "status", "INTEGER DEFAULT 1");
|
|
ensureColumn("entities", "is_deleted", "INTEGER NOT NULL DEFAULT 0");
|
|
ensureColumn("entities", "type_id", "TEXT NOT NULL DEFAULT 'country'");
|
|
ensureColumn("geometries", "is_deleted", "INTEGER NOT NULL DEFAULT 0");
|
|
ensureColumn("geometries", "binding", "TEXT");
|
|
dropGeometryDeprecatedColumnsIfExists();
|
|
migrateLegacyGeometryTypeTokens();
|
|
|
|
db.prepare(`
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_entities_slug
|
|
ON entities(slug)
|
|
`).run();
|
|
|
|
db.prepare(`
|
|
CREATE INDEX IF NOT EXISTS idx_entities_name
|
|
ON entities(name)
|
|
`).run();
|
|
|
|
db.prepare(`DROP INDEX IF EXISTS idx_entity_geometries_geometry_id`).run();
|
|
|
|
db.prepare(`
|
|
CREATE INDEX IF NOT EXISTS idx_entity_geometries_geometry_id
|
|
ON entity_geometries(geometry_id)
|
|
`).run();
|
|
|
|
db.prepare(`
|
|
CREATE INDEX IF NOT EXISTS idx_entity_geometries_entity_id
|
|
ON entity_geometries(entity_id)
|
|
`).run();
|
|
|
|
module.exports = db;
|
|
|
|
function ensureColumn(tableName, columnName, columnDefinition) {
|
|
const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
const hasColumn = columns.some((column) => column.name === columnName);
|
|
if (hasColumn) return;
|
|
db.prepare(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDefinition}`).run();
|
|
}
|
|
|
|
function dropGeometryDeprecatedColumnsIfExists() {
|
|
db.prepare(`DROP INDEX IF EXISTS idx_geometries_kind`).run();
|
|
|
|
const columns = db.prepare(`PRAGMA table_info(geometries)`).all();
|
|
const hasKind = columns.some((column) => column.name === "kind");
|
|
const hasLineMode = columns.some((column) => column.name === "line_mode");
|
|
if (!hasKind && !hasLineMode) return;
|
|
rebuildGeometriesTableToCurrentSchema(columns);
|
|
}
|
|
|
|
function rebuildGeometriesTableToCurrentSchema(existingColumns = null) {
|
|
const foreignKeysEnabled = Number(db.pragma("foreign_keys", { simple: true })) === 1;
|
|
const columns = existingColumns || db.prepare(`PRAGMA table_info(geometries)`).all();
|
|
const hasType = columns.some((column) => column.name === "type");
|
|
const hasIsDeleted = columns.some((column) => column.name === "is_deleted");
|
|
const hasBinding = columns.some((column) => column.name === "binding");
|
|
const hasTimeStart = columns.some((column) => column.name === "time_start");
|
|
const hasTimeEnd = columns.some((column) => column.name === "time_end");
|
|
const hasBBoxMinLng = columns.some((column) => column.name === "bbox_min_lng");
|
|
const hasBBoxMinLat = columns.some((column) => column.name === "bbox_min_lat");
|
|
const hasBBoxMaxLng = columns.some((column) => column.name === "bbox_max_lng");
|
|
const hasBBoxMaxLat = columns.some((column) => column.name === "bbox_max_lat");
|
|
const hasCreatedAt = columns.some((column) => column.name === "created_at");
|
|
const hasUpdatedAt = columns.some((column) => column.name === "updated_at");
|
|
|
|
const typeSelect = hasType ? "type" : "NULL";
|
|
const isDeletedSelect = hasIsDeleted ? "is_deleted" : "0";
|
|
const bindingSelect = hasBinding ? "binding" : "NULL";
|
|
const timeStartSelect = hasTimeStart ? "time_start" : "NULL";
|
|
const timeEndSelect = hasTimeEnd ? "time_end" : "NULL";
|
|
const bboxMinLngSelect = hasBBoxMinLng ? "bbox_min_lng" : "NULL";
|
|
const bboxMinLatSelect = hasBBoxMinLat ? "bbox_min_lat" : "NULL";
|
|
const bboxMaxLngSelect = hasBBoxMaxLng ? "bbox_max_lng" : "NULL";
|
|
const bboxMaxLatSelect = hasBBoxMaxLat ? "bbox_max_lat" : "NULL";
|
|
const createdAtSelect = hasCreatedAt ? "created_at" : "NULL";
|
|
const updatedAtSelect = hasUpdatedAt ? "updated_at" : "NULL";
|
|
if (foreignKeysEnabled) {
|
|
db.pragma("foreign_keys = OFF");
|
|
}
|
|
|
|
try {
|
|
const tx = db.transaction(() => {
|
|
db.prepare(`DROP TABLE IF EXISTS geometries_new`).run();
|
|
|
|
db.prepare(`
|
|
CREATE TABLE geometries_new (
|
|
id TEXT PRIMARY KEY,
|
|
type TEXT,
|
|
is_deleted INTEGER NOT NULL DEFAULT 0,
|
|
draw_geometry TEXT NOT NULL,
|
|
binding TEXT,
|
|
time_start INTEGER,
|
|
time_end INTEGER,
|
|
bbox_min_lng REAL,
|
|
bbox_min_lat REAL,
|
|
bbox_max_lng REAL,
|
|
bbox_max_lat REAL,
|
|
created_at TEXT,
|
|
updated_at TEXT
|
|
)
|
|
`).run();
|
|
|
|
db.prepare(`
|
|
INSERT INTO geometries_new (
|
|
id,
|
|
type,
|
|
is_deleted,
|
|
draw_geometry,
|
|
binding,
|
|
time_start,
|
|
time_end,
|
|
bbox_min_lng,
|
|
bbox_min_lat,
|
|
bbox_max_lng,
|
|
bbox_max_lat,
|
|
created_at,
|
|
updated_at
|
|
)
|
|
SELECT
|
|
id,
|
|
${typeSelect},
|
|
${isDeletedSelect},
|
|
draw_geometry,
|
|
${bindingSelect},
|
|
${timeStartSelect},
|
|
${timeEndSelect},
|
|
${bboxMinLngSelect},
|
|
${bboxMinLatSelect},
|
|
${bboxMaxLngSelect},
|
|
${bboxMaxLatSelect},
|
|
${createdAtSelect},
|
|
${updatedAtSelect}
|
|
FROM geometries
|
|
`).run();
|
|
|
|
db.prepare(`DROP TABLE geometries`).run();
|
|
db.prepare(`ALTER TABLE geometries_new RENAME TO geometries`).run();
|
|
});
|
|
|
|
tx();
|
|
} finally {
|
|
if (foreignKeysEnabled) {
|
|
db.pragma("foreign_keys = ON");
|
|
}
|
|
}
|
|
}
|
|
|
|
function migrateLegacyGeometryTypeTokens() {
|
|
const geometryColumns = db.prepare(`PRAGMA table_info(geometries)`).all();
|
|
const hasType = geometryColumns.some((column) => column.name === "type");
|
|
if (!hasType) return;
|
|
|
|
const tx = db.transaction(() => {
|
|
const legacyRows = db.prepare(`
|
|
SELECT
|
|
g.id,
|
|
(
|
|
SELECT e.type_id
|
|
FROM entity_geometries eg
|
|
JOIN entities e
|
|
ON e.id = eg.entity_id
|
|
AND e.is_deleted = 0
|
|
WHERE eg.geometry_id = g.id
|
|
ORDER BY eg.rowid ASC
|
|
LIMIT 1
|
|
) AS primary_entity_type
|
|
FROM geometries g
|
|
WHERE g.type IS NULL
|
|
OR TRIM(g.type) = ''
|
|
OR LOWER(TRIM(g.type)) IN ('line', 'path')
|
|
`).all();
|
|
|
|
const updateTypeStmt = db.prepare(`
|
|
UPDATE geometries
|
|
SET type = ?
|
|
WHERE id = ?
|
|
`);
|
|
|
|
for (const row of legacyRows) {
|
|
const semanticType = normalizeText(row.primary_entity_type);
|
|
updateTypeStmt.run(semanticType, row.id);
|
|
}
|
|
});
|
|
|
|
tx();
|
|
}
|
|
|
|
function normalizeText(value) {
|
|
if (value === undefined || value === null) return null;
|
|
const normalized = String(value).trim().toLowerCase();
|
|
return normalized.length ? normalized : null;
|
|
}
|