Files
temp-history-api/db/polygons.js
2026-04-17 20:55:33 +07:00

328 lines
12 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',
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");
dropEntityDeprecatedColumnsIfExists();
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 dropEntityDeprecatedColumnsIfExists() {
const columns = db.prepare(`PRAGMA table_info(entities)`).all();
const hasKind = columns.some((column) => column.name === "kind");
if (!hasKind) return;
rebuildEntitiesTableToCurrentSchema(columns);
}
function rebuildEntitiesTableToCurrentSchema(existingColumns = null) {
const foreignKeysEnabled = Number(db.pragma("foreign_keys", { simple: true })) === 1;
const columns = existingColumns || db.prepare(`PRAGMA table_info(entities)`).all();
const hasSlug = columns.some((column) => column.name === "slug");
const hasDescription = columns.some((column) => column.name === "description");
const hasTypeId = columns.some((column) => column.name === "type_id");
const hasStatus = columns.some((column) => column.name === "status");
const hasIsDeleted = columns.some((column) => column.name === "is_deleted");
const hasCreatedAt = columns.some((column) => column.name === "created_at");
const hasUpdatedAt = columns.some((column) => column.name === "updated_at");
const slugSelect = hasSlug ? "slug" : "NULL";
const descriptionSelect = hasDescription ? "description" : "NULL";
const typeIdSelect = hasTypeId ? "type_id" : "'country'";
const statusSelect = hasStatus ? "status" : "1";
const isDeletedSelect = hasIsDeleted ? "is_deleted" : "0";
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 entities_new`).run();
db.prepare(`
CREATE TABLE entities_new (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE,
description TEXT,
type_id TEXT NOT NULL DEFAULT 'country',
status INTEGER DEFAULT 1,
is_deleted INTEGER NOT NULL DEFAULT 0,
created_at TEXT,
updated_at TEXT
)
`).run();
db.prepare(`
INSERT INTO entities_new (
id,
name,
slug,
description,
type_id,
status,
is_deleted,
created_at,
updated_at
)
SELECT
id,
name,
${slugSelect},
${descriptionSelect},
${typeIdSelect},
${statusSelect},
${isDeletedSelect},
${createdAtSelect},
${updatedAtSelect}
FROM entities
`).run();
db.prepare(`DROP TABLE entities`).run();
db.prepare(`ALTER TABLE entities_new RENAME TO entities`).run();
});
tx();
} finally {
if (foreignKeysEnabled) {
db.pragma("foreign_keys = ON");
}
}
}
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;
}