sql >> Base de Datos >  >> RDS >> Database

Migración de AnswerHub a WordPress:una historia de 10 tecnologías

Recientemente lanzamos un nuevo sitio de soporte, donde puede hacer preguntas, enviar comentarios sobre productos o solicitudes de funciones, o abrir tickets de soporte. Parte del objetivo era centralizar todos los lugares donde estábamos ofreciendo asistencia a la comunidad. Esto incluyó el sitio de preguntas y respuestas de SQLPerformance.com, donde Paul White, Hugo Kornelis y muchos otros han estado ayudando a resolver las preguntas más complicadas sobre el ajuste de consultas y el plan de ejecución, desde febrero de 2013. Les digo con sentimientos encontrados que el El sitio de preguntas y respuestas ha sido cerrado.

Sin embargo, hay una ventaja. Ahora puede hacer esas preguntas difíciles en el nuevo foro de soporte. Si está buscando el contenido anterior, bueno, todavía está allí, pero se ve un poco diferente. Por una variedad de razones que no mencionaré hoy, una vez que decidimos cancelar el sitio de preguntas y respuestas original, finalmente decidimos simplemente alojar todo el contenido existente en un sitio de WordPress de solo lectura, en lugar de migrarlo al back-end. del nuevo sitio.

Esta publicación no trata sobre las razones detrás de esa decisión.

Me sentí muy mal por la rapidez con la que el sitio de respuestas tuvo que desconectarse, el DNS cambió y el contenido migró. Dado que se implementó un banner de advertencia en el sitio, pero AnswerHub en realidad no lo hizo visible, esto fue un shock para muchos usuarios. Así que quería asegurarme de mantener adecuadamente la mayor cantidad de contenido posible, y quería que fuera correcto. Esta publicación está aquí porque pensé que sería interesante hablar sobre el proceso real, cuántas piezas diferentes de tecnología estuvieron involucradas para llevarlo a cabo y mostrar el resultado. No espero que ninguno de ustedes se beneficie de este punto a punto, ya que esta es una ruta de migración relativamente oscura, pero más como un ejemplo de vincular un montón de tecnologías para realizar una tarea. También me sirve como un buen recordatorio de que muchas cosas no terminan siendo tan fáciles como parecen antes de comenzar.

El TL;DR es esto:dediqué mucho tiempo y esfuerzo a hacer que el contenido archivado se viera bien, aunque todavía estoy tratando de recuperar las últimas publicaciones que llegaron hacia el final. Usé estas tecnologías:

  1. Perl
  2. Servidor SQL
  3. PowerShell
  4. Transmitir (FTP)
  5. HTML
  6. CSS
  7. C#
  8. MarkdownSharp
  9. phpMyAdmin
  10. MySQL

De ahí el título. Si quieres una gran parte de los detalles sangrientos, aquí están. Si tiene alguna pregunta o comentario, comuníquese con nosotros o comente a continuación.

AnswerHub proporcionó un archivo de volcado de 665 MB de la base de datos MySQL que alojaba el contenido de preguntas y respuestas. Cada editor que probé se atragantó con él, así que primero tuve que dividirlo en un archivo por tabla usando este práctico script Perl de Jared Cheney. Las tablas que necesitaba se llamaban network11_nodes (preguntas, respuestas y comentarios), network11_authoritables (usuarios) y network11_managed_files (todos los archivos adjuntos, incluidas las cargas de planes):perl extract_sql.pl -t network11_nodes -r dump.sql>> nodes.sql
perl extract_sql.pl -t network11_authoritables -r dump.sql>> users.sql
perl extract_sql.pl -t red11_archivos_administrados -r dump.sql>> archivos.sql

Ahora, esos no fueron extremadamente rápidos de cargar en SSMS, pero al menos allí pude usar Ctrl +H para cambiar (por ejemplo) esto:

CREATE TABLE `network11_managed_files` (
  `c_id` bigint(20) NOT NULL,
  ...
);
 
INSERT INTO `network11_managed_files` (`c_id`, ...) VALUES (1, ...);

A esto:

CREATE TABLE dbo.files
(
  c_id bigint NOT NULL,
  ...
);
 
INSERT dbo.files (c_id, ...) VALUES (1, ...);

Entonces podría cargar los datos en SQL Server para poder manipularlos. Y créanme, lo manipulé.

A continuación, tuve que recuperar todos los archivos adjuntos. Mira, el archivo de volcado de MySQL que obtuve del proveedor contenía un montón de INSERT declaraciones, pero ninguno de los archivos de plan reales que los usuarios habían cargado; la base de datos solo tenía las rutas relativas a los archivos. Usé T-SQL para crear una serie de comandos de PowerShell que llamarían a Invoke-WebRequest para recuperar todos los archivos y almacenarlos localmente (muchas formas de despellejar a este gato, pero esto fue muy fácil). De esto:

SELECT 'Invoke-WebRequest -Uri '
  + '"$($url)' + RTRIM(c_id) + '-' + c_name + '"'
  + ' -OutFile "E:\s\temp\' + RTRIM(c_id) + '-' + c_name + '";'
  FROM dbo.files
  WHERE LOWER(c_mime_type) LIKE 'application/%';

Eso produjo este conjunto de comandos (junto con un comando previo para resolver este problema de TLS); todo funcionó bastante rápido, pero no recomiendo este enfoque para ninguna combinación de {conjunto masivo de archivos} y/o {bajo ancho de banda}:

$AllProtocols = [System.Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12';
[System.Net.ServicePointManager]::SecurityProtocol = $AllProtocols;
$u = "https://answers.sqlperformance.com/s/temp/";
 
Invoke-WebRequest -Uri "$($u)/1-proc.pesession"   -OutFile "E:\s\temp\1-proc.pesession";
Invoke-WebRequest -Uri "$($u)/14-test.pesession"  -OutFile "E:\s\temp\14-test.pesession";
Invoke-WebRequest -Uri "$($u)/15-a.QueryAnalysis" -OutFile "E:\s\temp\15-a.QueryAnalysis";
...

Esto descargó casi todos los archivos adjuntos, pero es cierto que se perdieron algunos debido a errores en el sitio anterior cuando se cargaron inicialmente. Por lo tanto, en el nuevo sitio, es posible que ocasionalmente vea una referencia a un archivo adjunto que no existe.

Luego usé Panic Transmit 5 para subir el temp carpeta al nuevo sitio, y ahora, cuando se carga el contenido, se vincula a /s/temp/1-proc.pesession seguirá funcionando.

A continuación, pasé a SSL. Para solicitar un certificado en el nuevo sitio de WordPress, tuvimos que actualizar el DNS de answers.sqlperformance.com para que apuntara al CNAME en nuestro host de WordPress, WPEngine. Fue una especie de huevo y gallina aquí:tuvimos que sufrir un tiempo de inactividad para las URL https, que fallarían por no tener certificado en el nuevo sitio. Esto estuvo bien porque el certificado en el sitio anterior había caducado, así que en realidad no estábamos peor. También tuve que esperar para hacer esto hasta que hube descargado todos los archivos del sitio anterior, porque una vez que el DNS cambiaba, no habría forma de acceder a ellos excepto a través de alguna puerta trasera.

Mientras esperaba que se propagara el DNS, comencé a trabajar en la lógica para extraer todas las preguntas, respuestas y comentarios en algo consumible en WordPress. Los esquemas de las tablas no solo eran diferentes a los de WordPress, sino que los tipos de entidades también son bastante diferentes. Mi visión era combinar cada pregunta, y cualquier respuesta o comentario, en una sola publicación.

La parte complicada es que la tabla de nodos solo contiene los tres tipos de contenido en la misma tabla, con referencias primarias y originales ("maestras"). Es probable que su código frontal use algún tipo de cursor para recorrer y mostrar el contenido en un orden jerárquico y cronológico. No tendría ese lujo en WordPress, así que tuve que unir el HTML de una sola vez. Solo como ejemplo, así es como se veían los datos:

SELECT c_type, c_id, c_parent, oParent = c_originalParent, c_creation_date, c_title
  FROM dbo.nodes 
  WHERE c_originalParent = 285;
 
/*
c_type      c_id    c_parent  oParent  c_creation_date   accepted  c_title
----------  ------  --------  -------  ----------------  --------  -------------------------
question    285     NULL      285      2013-02-13 16:30            why is the MERGE JOIN ...
answer      287     285       285      2013-02-14 01:15  1         NULL
comment     289     285       285      2013-02-14 13:35            NULL
answer      293     285       285      2013-02-14 18:22            NULL
comment     294     287       285      2013-02-14 18:29            NULL
comment     298     285       285      2013-02-14 20:40            NULL
comment     299     298       285      2013-02-14 18:29            NULL
*/

No podía ordenar por identificación, tipo o padre, ya que a veces un comentario vendría más tarde en una respuesta anterior, la primera respuesta no siempre sería la respuesta aceptada, y así sucesivamente. Quería esta salida (donde ++ representa un nivel de sangría):

/*
c_type        c_id    c_parent  oParent  c_creation_date   reason
----------    ------  --------  -------  ----------------  -------------------------
question      285     NULL      285      2013-02-13 16:30  question is ALWAYS first
++comment     289     285       285      2013-02-14 13:35  comments on the question before answers
answer        287     285       285      2013-02-14 01:15  first answer (accepted = 1)
++comment     294     287       285      2013-02-14 18:29  first comment on first answer
++comment     298     287       285      2013-02-14 20:40  second comment on first answer
++++comment   299     298       285      2013-02-14 18:29  reply to second comment on first answer
answer        293     285       285      2013-02-14 18:22  second answer
*/

Empecé a escribir un CTE recursivo y, parcialmente Debido a demasiado Rekorderlig esa noche, pedí la ayuda de mi compañero Gerente de Producto, Andy Mallon (@AMtwo). Me ayudó a preparar esta consulta, que devolvería las publicaciones en su orden de visualización correcto (y puede probar este fragmento, cambiando los padres y/o la respuesta aceptada, para ver que todavía se devolverá el orden correcto):

DECLARE @foo TABLE
(
  c_type varchar(255), 
  c_id int, 
  c_parent int, 
  oParent int,
  accepted bit
);
 
INSERT @foo(c_type, c_id, c_parent, oParent, accepted) VALUES
('question', 285, NULL, 285, 0),
('answer',   287, 285 , 285, 1),
('comment',  289, 285 , 285, 0),
('comment',  294, 287 , 285, 0),
('comment',  298, 287 , 285, 0),
('comment',  299, 298 , 285, 0),
('answer',   293, 285 , 285, 0);
 
;WITH cte AS 
(
  SELECT 
    lvl = 0,
    f.c_type,
    f.c_id, f.c_parent, f.oParent,
    Sort = CONVERT(varchar(255),RIGHT('00000' + CONVERT(varchar(5),f.c_id),5))
  FROM @foo AS f WHERE f.c_parent IS NULL
  UNION ALL
  SELECT 
    lvl = c.lvl + 1,
    c_type = CONVERT(varchar(255), CASE
        WHEN f.accepted = 1 THEN 'accepted answer'
        WHEN f.c_type = 'comment' THEN c.c_type + ' ' + f.c_type
        ELSE f.c_type
      END),
    f.c_id, f.c_parent, f.oParent,
    Sort = CONVERT(varchar(255),c.Sort + RIGHT('00000' + CONVERT(varchar(5),f.c_id),5))
  FROM @foo AS f INNER JOIN cte AS c ON c.c_id = f.c_parent
)
SELECT lvl = CASE lvl WHEN 0 THEN 1 ELSE lvl END, c_type, c_id, c_parent, oParent, Sort
FROM cte
ORDER BY 
  oParent,
  CASE
    WHEN c_type LIKE 'question%'        THEN 1 -- it's a question *or* a comment on the question
    WHEN c_type LIKE 'accepted answer%' THEN 2 -- accepted answer *or* comment on accepted answer
    ELSE 3 END,
  Sort;

Resultados:

/*
lvl  c_type                            c_id        c_parent    oParent     Sort
---- --------------------------------- ----------- ----------- ----------- --------------------
1    question                          285         NULL        285         00285               
1    question comment                  289         285         285         0028500289          
1    accepted answer                   287         285         285         0028500287          
2    accepted answer comment           294         287         285         002850028700294     
2    accepted answer comment           298         287         285         002850028700298     
3    accepted answer comment comment   299         298         285         00285002870029800299
1    answer                            293         285         285         0028500293     
*/

Genio. Revisé una docena más o menos y me alegré de pasar al siguiente paso. Le he dado las gracias a Andy efusivamente, varias veces, pero déjame hacerlo de nuevo:¡Gracias, Andy!

Ahora que podía devolver todo el conjunto en el orden que me gustaba, tenía que manipular el resultado para aplicar elementos HTML y nombres de clase que me permitieran marcar preguntas, respuestas, comentarios y sangría de manera significativa. El objetivo final era una salida que se veía así (y tenga en cuenta que este es uno de los casos más simples):

<div class="question">
  <span class="authorq" title=" Author : author name ">
    <i class="fas fa-user"></i>Author name</span> 
  <span class="createdq" title=" February 13th, 2013 ">
    <i class="fas fa-calendar-alt"></i>2013-02-13 16:30:36</span>
 
  <div class=mainbodyq>I don't understand why the merge operator is passing over 4million 
  rows to the hash match operator when there is only 41K and 19K from other operators.
 
	<div class=attach><i class="fas fa-file"></i>
	  <a target="_blank" href="/s/temp/254-tmp4DA0.queryanalysis" rel="noopener noreferrer">
      /s/temp/254-tmp4DA0.queryanalysis</a>
	</div>
  </div>
 
  <div class="comment indent1 ">
    <div class=linecomment>
	  <span class="authorc" title=" Author : author name ">
	    <i class="fas fa-user"></i>author name</span>
	  <span class="createdc" title=" February 14th, 2013 ">
	    <i class="fas fa-calendar-alt"></i>2013-02-14 13:35:39</span>
	</div>
    <div class=mainbodyc>
	  I am still trying to understand the significant amount of rows from the MERGE operator. 
	  Unless it's a result of a Cartesian product from the two inputs then finally the WHERE 
	  predicate is applied to filter out the unmatched rows leaving the 4 million row count.
    </div>
  </div>
  <div class="answer indent1 [accepted]">
    <div class=lineanswer>
	  <span class="authora" title=" Author : author name ">
	    <i class="fas fa-user"></i>author name</span>
	  <span class="createda" title=" February 14th, 2013 ">
	    <i class="fas fa-calendar-alt"></i>2013-02-14 01:15:42</span>
	</div>
    <div class=mainbodya>
	    The reason for the large number of rows can be seen in the Plan Explorer tool tip for 
		the Merge Join operator:
 
	    <img src="/s/temp/259-sp.png" alt="Merge Join tool tip" />
	  	...
	</div>
  </div>
</div>

No pasaré por la ridícula cantidad de iteraciones por las que tuve que pasar para llegar a una forma confiable de ese resultado para los más de 5000 elementos (lo que se tradujo en casi 1000 publicaciones una vez que todo estuvo pegado). Además de eso, necesitaba generarlos en forma de INSERT declaraciones que luego podía pegar en phpMyAdmin en el sitio de WordPress, lo que significaba adherirse a su extraño diagrama de sintaxis. Esas declaraciones debían incluir otra información adicional requerida por WordPress, pero no presente o precisa en los datos de origen (como post_type ). Y esa consola de administración se agotaba con demasiados datos, por lo que tuve que dividirla en ~750 inserciones a la vez. Aquí está el procedimiento con el que terminé (esto no es realmente para aprender nada específico, solo una demostración de cuánta manipulación de los datos importados fue necesaria):

CREATE /* OR ALTER */ PROCEDURE dbo.BuildMySQLInserts
  @LowerBound int = 1, 
  @UpperBound int = 750
AS
BEGIN
  SET NOCOUNT ON;
 
  ;WITH CTE AS 
  (
    SELECT lvl = 0,
            [type] = CONVERT(varchar(100),f.[type]),
            f.id,
            f.parent,
            f.master_parent,
            created = CONVERT(char(10), f.created, 120) + ' ' 
			        + CONVERT(char(8),  f.created, 108),
            f.state,
            Sort = CONVERT(varchar(100),RIGHT('0000000000' 
			     + CONVERT(varchar(10),f.id),10))
    FROM dbo.foo AS f
    WHERE f.type = 'question' 
      AND master_parent BETWEEN @LowerBound AND @UpperBound
    UNION ALL
    SELECT lvl = c.lvl + 1,
            CONVERT(varchar(100),CASE
                WHEN f.[state] = '[accepted]' THEN 'accepted answer'
                WHEN f.type = 'comment' THEN c.type + ' ' + f.type
                ELSE f.type
            END),
            f.id,
            f.parent,
            f.master_parent,
            created = CONVERT(char(10), f.created, 120) + ' ' 
			        + CONVERT(char(8), f.created, 108),
            f.state,
            Sort = CONVERT(varchar(100),c.sort + RIGHT('0000000000' 
			     + CONVERT(varchar(10),f.id),10))
    FROM dbo.foo AS f
    JOIN CTE AS c ON c.id = f.parent
)
SELECT 
  master_parent, 
  prefix = CASE WHEN lvl = 0 THEN 
    CONVERT(varchar(11), master_parent) + ', 3, ''' + created + ''', ''' 
	+ created + ''',''' END, 
  bodypre = '<div class="' + COALESCE(c_type, RTRIM(LEFT([type],8))) 
	  + CASE WHEN c_type <> 'question' THEN ' indent' + RTRIM(lvl) 
	  + COALESCE(' ' + [state], '') ELSE '' END + '">'
	  + CASE WHEN c_type <> 'question' THEN 
	    '<div class=line' + c_type + '>' ELSE '' END 
	  + '<span class="author' + LEFT(c_type, 1) + '" title=" Author : ' 
	  + REPLACE(REPLACE(Fullname,'''','\'''),'"','') 
	  + ' "><i class="fas fa-user"></i>' + REPLACE(Fullname,'''','\''') --"
	  + '</span> <span class="created' + LEFT(c_type,1) + '" title=" ' 
	  + DATENAME(MONTH, c_creation_date) + ' ' + RTRIM(DAY(c_creation_date)) 
	  + CASE 
        WHEN DAY(c_creation_date) IN (1,21,31) THEN 'st'
        WHEN DAY(c_creation_date) IN (2,22) THEN 'nd'
        WHEN DAY(c_creation_date) IN (3,23) THEN 'rd' ELSE 'th' END
        + ', ' + RTRIM(YEAR(c_creation_date)) 
      + ' "><i class="fas fa-calendar-alt"></i>' + created + '</span>'
      + CASE WHEN c_type <> 'question' THEN '</div>' ELSE '' END,
  body = '<div class=mainbody' + left(c_type,1) + '>' 
	  + REPLACE(REPLACE(c_body, char(39), '\' + char(39)), '’', '\' + char(39)),
  bodypost = COALESCE(urls, '') + '</div></div>',--' 
	  + CASE WHEN c_type = 'question' THEN '</div>' ELSE '' END, 
  suffix = ''',''' + REPLACE(n.c_title, '''', '\''') + ''','''',''publish'',
	  ''closed'',''closed'','''',''' + REPLACE(n.c_plug, '''', '\''') 
	  + ''','''','''',''' + created + ''',''' + created + ''','''',0,
	  ''https://answers.sqlperformance.com/?p=' + CONVERT(varchar(11), master_parent) 
	  + ''', 0, ''post'','''',0);',
  rn = RTRIM(ROW_NUMBER() OVER (PARTITION BY master_parent 
      ORDER BY master_parent,
      CASE
        WHEN [type] LIKE 'question%' THEN 1
        WHEN [type] LIKE 'accepted answer%' THEN 2
        ELSE 3
      END,
      Sort)), 
  c = RTRIM(COUNT(*) OVER (PARTITION BY master_parent))
FROM CTE
LEFT OUTER JOIN dbo.network11_nodes AS n
ON cte.id = n.c_id
LEFT OUTER JOIN dbo.Users AS u
ON n.c_author = u.UserID
LEFT OUTER JOIN 
(
  SELECT NodeID, urls = STRING_AGG('<div class=attach>
    <i class="fas fa-file' 
	+ CASE WHEN c_mime_type IN ('image/jpeg','image/png') 
      THEN '-image' ELSE '' END 
    + '"></i><a target="_blank" href=' + url + ' rel="noopener noreferrer">' + url + '</a></div>', '\n') 
  FROM dbo.Attachments 
  GROUP BY NodeID
) AS a
ON n.c_id = a.NodeID
ORDER BY master_parent,
  CASE
    WHEN [type] LIKE 'question%' THEN 1
    WHEN [type] LIKE 'accepted answer%' THEN 2
    ELSE 3
  END,
  Sort;
END
GO

El resultado de eso no está completo y aún no está listo para incluirse en WordPress:

Salida de muestra (haga clic para ampliar)

Necesitaría ayuda adicional de C# para convertir el contenido real (incluida la reducción) en HTML y CSS que podría controlar mejor, y escribir el resultado (un montón de INSERT declaraciones que incluían un montón de código HTML) a archivos en el disco que podía abrir y pegar en phpMyAdmin. Para el HTML, texto sin formato + descuento que comenzaba así:

Hay una [publicación de blog aquí][1] que habla de ello, y también [esta publicación](https://somewhere).

SELECCIONA algo de dbo.sometable;

[1]:https://otro lugar

Tendría que convertirse en esto:

Hay una publicación de blog aquí que habla de ello, y también esta publicación .

SELECCIONE algo de dbo.sometable;

Para lograr esto, recluté la ayuda de MarkdownSharp, una biblioteca de código abierto que se originó en Stack Overflow y que maneja gran parte de la conversión de rebajas a HTML. Se ajustaba bien a mis necesidades, pero no perfecto; Todavía tendría que realizar más manipulaciones:

  • MarkdownSharp no permite cosas como target=_blank , por lo que tendría que inyectarlos yo mismo después del procesamiento;
  • code (cualquier cosa prefijada con cuatro espacios) hereda
    using System.Text;
    using System.Data;
    using System.Data.SqlClient;
    using MarkdownSharp;
    using System.IO;
     
    namespace AnswerHubMigrator
    {
      class Program
      {
        static void Main(string[] args)
        {
          StringBuilder output;
          string suffix = "";
          string thisfile = "";
     
          // pass two arguments on the command line, e.g. 1, 750
          int LowerBound = int.Parse(args[0]);
          int UpperBound = int.Parse(args[1]);
     
          // auto-expand URLs, and only accept bold/italic markdown
          // when it completely surrounds an entire word
          var options = new MarkdownOptions
          {
            AutoHyperlink = true,
            StrictBoldItalic = true
          };
          MarkdownSharp.Markdown mark = new MarkdownSharp.Markdown(options);
     
          using (var conn = new SqlConnection("Server=.\\SQL2017;Integrated Security=true"))
          using (var cmd = new SqlCommand("MigrateDB.dbo.BuildMySQLInserts", conn))
          {
     
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.Add("@LowerBound", SqlDbType.Int).Value = LowerBound;
            cmd.Parameters.Add("@UpperBound", SqlDbType.Int).Value = UpperBound;
            conn.Open();
            using (var reader = cmd.ExecuteReader())
            {
              // use a StringBuilder to dump output to a file
              output = new StringBuilder();
              while (reader.Read())
              {
                // on first pass, make a new delete/insert
                // delete is to make the commands idempotent
                if (reader["rn"].Equals("1"))
                {
     
                  // for each master parent, I would create a
                  // new WordPress post, inheriting the parent ID
                  output.Append("DELETE FROM `wp_posts` WHERE ID = ");
                  output.Append(reader["master_parent"].ToString());
                  output.Append("; INSERT INTO `wp_posts` (`ID`, `post_author`, ");
                  output.Append("`post_date`, `post_date_gmt`, `post_content`, ");
                  output.Append("`post_title`, `post_excerpt`, `post_status`, ");
                  output.Append("`comment_status`, `ping_status`, `post_password`,");
                  output.Append(" `post_name`, `to_ping`, `pinged`, `post_modified`,");
                  output.Append(" `post_modified_gmt`, `post_content_filtered`, ");
                  output.Append("`post_parent`, `guid`, `menu_order`, `post_type`, ");
                  output.Append("`post_mime_type`, `comment_count`) VALUES (");
     
                  // I'm sure some of the above columns are optional, but identifying
                  // those would not be a valuable use of time IMHO
     
                  output.Append(reader["prefix"]);
     
                  // hold on to the additional values until last row
                  suffix = reader["suffix"].ToString();
                }
     
                // manipulate the body content to be WordPress and INSERT statement-friendly
                string body = reader["body"].ToString().Replace(@"\n", "\n");
                body = mark.Transform(body).Replace("href=", "target=_blank href=");
                body = body.Replace("<p>", "").Replace("</p>", "");
                body = body.Replace("<pre><code>", "<pre lang=\"tsql\">");
                body = body.Replace("</code></"+"pre>", "</"+"pre>");
                body = body.Replace(@"'", "\'").Replace(@"’", "\'");
     
                body = reader["bodypre"].ToString() + body.Replace("\n", @"\n");
                body += reader["bodypost"].ToString();
                body = body.Replace("&lt;", "<").Replace("&gt;", ">");
                output.Append(body);
     
                // if we are on the last row, add additional values from the first row
                if (reader["c"].Equals(reader["rn"]))
                {
                  output.Append(suffix);
                }
              }
     
              thisfile = UpperBound.ToString();
              using (StreamWriter w = new StreamWriter(@"C:\wp\" + thisfile + ".sql"))
              {
                w.WriteLine(output);
                w.Flush();
              }
            }
          }
        }
      }
    }

    Sí, ese es un montón de código feo, pero finalmente me llevó al conjunto de resultados que no harían vomitar a phpMyAdmin, y que WordPress se presentaría bien (suficiente). Simplemente llamé al programa C# varias veces con los diferentes rangos de parámetros:

    AnswerHubMigrator    1  750
    AnswerHubMigrator  751 1500
    AnswerHubMigrator 1501 2250
    ...

    Luego abrí cada uno de los archivos, los pegué en phpMyAdmin y presioné IR:

    phpMyAdmin (haga clic para agrandar)

    Por supuesto, tuve que agregar algo de CSS dentro de WordPress para ayudar a diferenciar entre preguntas, comentarios y respuestas, y también sangrar los comentarios para mostrar las respuestas tanto a las preguntas como a las respuestas, anidar los comentarios que respondían a los comentarios, etc. Así es como se ve un extracto cuando profundiza en las preguntas de un mes:

    Cuadro de preguntas (haga clic para agrandar)

    Y luego una publicación de ejemplo, que muestra imágenes incrustadas, varios archivos adjuntos, comentarios anidados y una respuesta:

    Pregunta y respuesta de muestra (haga clic para ir allí)

    Todavía estoy tratando de recuperar algunas publicaciones que se enviaron al sitio después de que se realizó la última copia de seguridad, pero lo invito a navegar. Háganos saber si encuentra algo que falta o está fuera de lugar, o simplemente díganos que el contenido aún le es útil. Esperamos reintroducir la funcionalidad de carga de planes desde Plan Explorer, pero requerirá un poco de trabajo de API en el nuevo sitio de soporte, por lo que no tengo una ETA para usted hoy.

      Respuestas.SQLPerformance.com