DEV Community

Cover image for Convirtiendo Tablas Web a Sentencias SQL INSERT
circobit
circobit

Posted on

Convirtiendo Tablas Web a Sentencias SQL INSERT

Tienes una tabla en una página web. La necesitas en tu base de datos.

El enfoque manual: copiar a Excel, limpiar, exportar CSV, escribir CREATE TABLE a mano, usar LOAD DATA o COPY, depurar los errores.

El enfoque mejor: generar SQL completo directamente — CREATE TABLE con tipos inferidos, sentencias INSERT con escape adecuado.

Aquí te muestro cómo construir un conversor de tabla web a SQL.

El Formato de Salida

Una exportación SQL completa debería incluir:

-- Exportado con HTML Table Exporter PRO

CREATE TABLE products (
  product_id INTEGER,
  name TEXT,
  price REAL,
  in_stock TEXT
);

INSERT INTO products (product_id, name, price, in_stock) VALUES
  (1, 'Widget', 29.99, 'true'),
  (2, 'Gadget', 49.99, 'false'),
  (3, 'O''Brien''s Special', 19.99, 'true');
Enter fullscreen mode Exit fullscreen mode

Requisitos clave:

  1. Nombre de tabla válido (identificador SQL seguro)
  2. Nombres de columna válidos (sin espacios, sin caracteres especiales)
  3. Tipos de columna apropiados (INTEGER, REAL, TEXT)
  4. Valores correctamente escapados (comillas simples duplicadas)
  5. Manejo de NULL

Paso 1: Sanitizar Identificadores

Los identificadores SQL (nombres de tabla y columna) tienen reglas estrictas:

  • Sin espacios ni caracteres especiales
  • No pueden empezar con un dígito
  • Deberían ser minúsculas para portabilidad
function sanitizeSqlIdentifier(name, fallback) {
  let id = (name || "").toString().trim();

  if (!id) id = fallback || "col";

  id = id
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")  // Remover acentos (café → cafe)
    .toLowerCase()
    .replace(/[^a-z0-9_]+/g, "_")     // Reemplazar caracteres inválidos con guión bajo
    .replace(/^_+|_+$/g, "");          // Recortar guiones bajos al inicio/final

  // Los identificadores SQL no pueden empezar con un dígito
  if (/^[0-9]/.test(id)) {
    id = "_" + id;
  }

  if (!id) id = fallback || "col";
  return id;
}
Enter fullscreen mode Exit fullscreen mode

Ejemplos:

  • "Nombre Producto" → "nombre_producto"
  • "Precio ($)" → "precio"
  • "2024 Ingresos" → "_2024_ingresos"
  • "Préço" → "preco"

Paso 2: Generar Nombres de Columna Únicos

Las tablas pueden tener encabezados duplicados. SQL no puede tener nombres de columna duplicados.

function generateColumnNames(headerRow) {
  const usedNames = new Set();

  return headerRow.map((header, index) => {
    const base = sanitizeSqlIdentifier(header, `col_${index + 1}`);
    let candidate = base;
    let counter = 1;

    while (usedNames.has(candidate)) {
      candidate = `${base}_${counter}`;
      counter++;
    }

    usedNames.add(candidate);
    return candidate;
  });
}
Enter fullscreen mode Exit fullscreen mode

Ejemplos:

  • ["Nombre", "Nombre", "Valor"] → ["nombre", "nombre_1", "valor"]
  • ["", "", "Datos"] → ["col_1", "col_2", "datos"]

Paso 3: Inferir Tipos de Columna

SQL tiene tres tipos principales que nos importan:

  • INTEGER: Números enteros
  • REAL: Números decimales
  • TEXT: Todo lo demás

La inferencia muestrea valores de la columna y elige el tipo más específico que se ajuste a todos los valores:

function inferSqlColumnTypes(rows, headerRowIndex = 0) {
  const headerRow = rows[headerRowIndex] || [];
  const dataRows = rows.slice(headerRowIndex + 1);

  const colCount = headerRow.length;
  const types = new Array(colCount).fill("TEXT");

  for (let col = 0; col < colCount; col++) {
    // Muestrear hasta 50 valores no vacíos
    const values = [];

    for (let r = 0; r < dataRows.length && values.length < 50; r++) {
      const cell = dataRows[r][col];
      const v = cell != null ? String(cell).trim() : "";
      if (v !== "") values.push(v);
    }

    if (values.length === 0) {
      types[col] = "TEXT";
      continue;
    }

    // Verificar si todos los valores son enteros
    let allInt = true;
    let allNumeric = true;

    for (const v of values) {
      if (!/^[-+]?\d+$/.test(v)) {
        allInt = false;
      }
      if (!/^[-+]?\d+([.,]\d+)?$/.test(v)) {
        allNumeric = false;
      }
    }

    if (allInt) {
      types[col] = "INTEGER";
    } else if (allNumeric) {
      types[col] = "REAL";
    } else {
      types[col] = "TEXT";
    }
  }

  return types;
}
Enter fullscreen mode Exit fullscreen mode

Decisiones clave:

  • Muestrear solo 50 valores para rendimiento en tablas grandes
  • INTEGER es más estricto que REAL (no se permiten decimales)
  • Si CUALQUIER valor no coincide, caer a TEXT
  • Las columnas vacías tienen TEXT por defecto

Paso 4: Escapar Valores

Las cadenas SQL requieren escapar comillas simples duplicándolas:

function sqlEscapeValue(raw, type) {
  // Manejo de NULL
  if (raw == null) return "NULL";

  const v = String(raw).trim();
  if (v === "") return "NULL";

  // Para tipos numéricos, intentar devolver número sin comillas
  if (type === "INTEGER" || type === "REAL") {
    const normalized = v.replace(",", ".");  // Manejar formato decimal UE
    const num = Number(normalized);

    if (!Number.isNaN(num) && Number.isFinite(num)) {
      return normalized;
    }
    // Caer al manejo TEXT si no es un número válido
  }

  // TEXT o fallback: escapar comillas simples
  const escaped = v.replace(/'/g, "''");
  return `'${escaped}'`;
}
Enter fullscreen mode Exit fullscreen mode

Ejemplos:

  • "Hola"'Hola'
  • "O'Brien"'O''Brien'
  • "Es \"citado\""'Es "citado"'
  • 123 (INTEGER) → 123
  • 45.67 (REAL) → 45.67
  • ""NULL
  • nullNULL

Paso 5: Generar el SQL Completo

function tableToSqlString(tableInfo) {
  const rows = tableInfo.rows || [];
  if (!rows.length) return "";

  const headerRowIndex = tableInfo.headerRowIndex || 0;
  const headerRow = rows[headerRowIndex];
  const dataRows = rows.slice(headerRowIndex + 1);

  if (!headerRow) return "";

  // Generar nombres de columna seguros
  const columnNames = generateColumnNames(headerRow);

  // Inferir tipos de columna
  const types = inferSqlColumnTypes(rows, headerRowIndex);

  // Generar nombre de tabla seguro
  const rawTableName = tableInfo.slug || tableInfo.name || "table";
  let tableName = sanitizeSqlIdentifier(rawTableName, "table");
  if (!tableName) tableName = "table_export";

  // CONSTRUIR CREATE TABLE
  const createLines = columnNames.map((col, i) => 
    `  ${col} ${types[i] || "TEXT"}`
  );
  const createStmt = `CREATE TABLE ${tableName} (\n${createLines.join(",\n")}\n);`;

  // CONSTRUIR SENTENCIAS INSERT
  const insertHeader = `INSERT INTO ${tableName} (${columnNames.join(", ")}) VALUES`;

  const valueLines = dataRows.map(row => {
    const values = columnNames.map((_, i) => {
      const cell = row[i];
      const type = types[i] || "TEXT";
      return sqlEscapeValue(cell, type);
    });
    return `  (${values.join(", ")})`;
  });

  // COMBINAR
  let sql = `-- Exportado con HTML Table Exporter PRO\n\n${createStmt}\n\n`;

  if (valueLines.length) {
    sql += `${insertHeader}\n${valueLines.join(",\n")};\n`;
  }

  return sql;
}
Enter fullscreen mode Exit fullscreen mode

Ejemplo de Salida

Tabla de entrada:

Producto Precio Cant
Widget 29.99 100
O'Brien's 19.99 50

Salida:

-- Exportado con HTML Table Exporter PRO

CREATE TABLE products (
  producto TEXT,
  precio REAL,
  cant INTEGER
);

INSERT INTO products (producto, precio, cant) VALUES
  ('Widget', 29.99, 100),
  ('O''Brien''s', 19.99, 50);
Enter fullscreen mode Exit fullscreen mode

Manejando Casos Extremos

Tablas Vacías

if (!rows.length) return "";
if (!headerRow) return "";
Enter fullscreen mode Exit fullscreen mode

Devolver string vacío en vez de SQL inválido.

Columnas Completamente NULL

Las columnas con solo valores vacíos/null reciben tipo TEXT. La función de inferencia maneja esto:

if (values.length === 0) {
  types[col] = "TEXT";
  continue;
}
Enter fullscreen mode Exit fullscreen mode

Formatos Numéricos Mixtos

El escapador normaliza decimales con coma:

const normalized = v.replace(",", ".");
Enter fullscreen mode Exit fullscreen mode

"1,234.56" queda igual. "1.234,56" (formato UE) debería normalizarse antes de llegar al generador SQL — ese es el trabajo de los presets de limpieza.

Valores Muy Largos

Las columnas TEXT en SQLite/PostgreSQL manejan largo arbitrario. No se necesita truncar. Para los límites de VARCHAR de MySQL, necesitarás especificar el largo o usar TEXT explícitamente.

Compatibilidad con Bases de Datos

El SQL generado es intencionalmente simple:

Funcionalidad SQLite PostgreSQL MySQL
CREATE TABLE
INTEGER/REAL/TEXT
INSERT multi-fila
Escape comilla simple

Para funcionalidades específicas de base de datos (constraints, índices, esquemas), extenderías el generador. Para importación básica de datos, esto funciona en todas partes.

Usando la Exportación

# SQLite
sqlite3 mydb.db < export.sql

# PostgreSQL
psql -d mydb -f export.sql

# MySQL
mysql mydb < export.sql
Enter fullscreen mode Exit fullscreen mode

O pégalo directamente en tu cliente de base de datos.

Para más sobre la ruta de exportación CSV (cuando no necesitas SQL), consulta nuestra guía sobre exportar tablas HTML a CSV.


¿Necesitas exportaciones SQL sin escribir código? Conoce más en gauchogrid.com/es/html-table-exporter o pruébalo gratis en la Chrome Web Store.

Top comments (0)