sql >> Database >  >> RDS >> Database

Migreren van AnswerHub naar WordPress:A Tale of 10 Technologies

We hebben onlangs een nieuwe ondersteuningssite gelanceerd, waar u vragen kunt stellen, productfeedback of functieverzoeken kunt indienen of ondersteuningstickets kunt openen. Een deel van het doel was om alle plaatsen waar we hulp aan de gemeenschap boden, te centraliseren. Dit omvatte de Q&A-site SQLPerformance.com, waar Paul White, Hugo Kornelis en vele anderen hebben geholpen bij het oplossen van uw meest gecompliceerde vragen over het afstemmen van zoekopdrachten en uitvoeringsplannen, helemaal terug tot februari 2013. Ik vertel u met gemengde gevoelens dat de Q&A-site is gesloten.

Er is echter een voordeel. Je kunt die lastige vragen nu stellen op het nieuwe ondersteuningsforum. Als je op zoek bent naar de oude inhoud, wel, die is er nog steeds, maar het ziet er een beetje anders uit. Om verschillende redenen waar ik vandaag niet op in zal gaan, hebben we, toen we besloten om de originele Q&A-site te beëindigen, uiteindelijk besloten om alle bestaande inhoud eenvoudigweg op een alleen-lezen WordPress-site te hosten, in plaats van deze naar de back-end te migreren van de nieuwe site.

Dit bericht gaat niet over de redenen achter die beslissing.

Ik voelde me erg slecht over hoe snel de antwoordensite offline moest gaan, de DNS overschakelde en de inhoud werd gemigreerd. Omdat er een waarschuwingsbanner op de site was geïmplementeerd, maar AnswerHub deze niet echt zichtbaar maakte, was dit een schok voor veel gebruikers. Dus ik wilde ervoor zorgen dat ik zoveel mogelijk van de inhoud goed bewaarde, en ik wilde dat het goed was. Dit bericht is hier omdat ik dacht dat het interessant zou zijn om te praten over het eigenlijke proces, hoeveel verschillende stukjes technologie erbij betrokken waren om het voor elkaar te krijgen, en om te pronken met het resultaat. Ik verwacht niet dat iemand van jullie hier profijt van heeft, aangezien dit een relatief obscuur migratiepad is, maar meer als een voorbeeld van het samenbinden van een heleboel technologieën om een ​​taak te volbrengen. Het dient ook als een goede herinnering voor mezelf dat veel dingen niet zo eenvoudig zijn als ze klinken voordat je begint.

De TL;DR is dit:ik heb veel tijd en moeite gestoken om de gearchiveerde inhoud er goed uit te laten zien, hoewel ik nog steeds probeer de laatste paar berichten te herstellen die tegen het einde zijn binnengekomen. Ik heb deze technologieën gebruikt:

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

Vandaar de titel. Als je een groot deel van de bloederige details wilt, hier zijn ze. Als je vragen of feedback hebt, neem dan contact met ons op of reageer hieronder.

AnswerHub heeft een dumpbestand van 665 MB geleverd uit de MySQL-database die de Q&A-inhoud heeft gehost. Elke editor die ik probeerde, verslikte zich erin, dus ik moest het eerst opdelen in een bestand per tabel met dit handige Perl-script van Jared Cheney. De tabellen die ik nodig had, heetten network11_nodes (vragen, antwoorden en opmerkingen), network11_authoritables (gebruikers), en network11_managed_files (alle bijlagen, inclusief uploads van abonnementen):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 network11_managed_files -r dump.sql>> files.sql

Nu waren die niet extreem snel te laden in SSMS, maar daar kon ik tenminste Ctrl gebruiken +H om (bijvoorbeeld) dit te wijzigen:

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

Hierop:

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

Dan kon ik de gegevens in SQL Server laden, zodat ik het kon manipuleren. En geloof me, ik heb het gemanipuleerd.

Vervolgens moest ik alle bijlagen ophalen. Kijk, het MySQL-dumpbestand dat ik van de leverancier kreeg, bevatte een ontelbaar aantal INSERT verklaringen, maar geen van de daadwerkelijke planbestanden die gebruikers hadden geüpload - de database had alleen de relatieve paden naar de bestanden. Ik heb T-SQL gebruikt om een ​​reeks PowerShell-opdrachten te bouwen die Invoke-WebRequest zouden aanroepen om alle bestanden op te halen en ze lokaal op te slaan (veel manieren om deze kat te villen, maar dit was doodeenvoudig). Hieruit:

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/%';

Dat leverde deze reeks opdrachten op (samen met een pre-opdracht om dit TLS-probleem op te lossen); het hele ding liep vrij snel, maar ik raad deze aanpak niet aan voor een combinatie van {massive set of files} en/of {low bandbreedte}:

$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";
...

Hiermee werden bijna alle bijlagen gedownload, maar toegegeven, sommige werden gemist vanwege fouten op de oude site toen ze voor het eerst werden geüpload. Op de nieuwe site ziet u dus af en toe een verwijzing naar een bijlage die niet bestaat.

Daarna gebruikte ik Panic Transmit 5 om de temp . te uploaden map naar de nieuwe site, en nu wanneer de inhoud wordt geüpload, links naar /s/temp/1-proc.pesession zal blijven werken.

Vervolgens ben ik overgestapt op SSL. Om een ​​certificaat op de nieuwe WordPress-site aan te vragen, moesten we de DNS voor answer.sqlperformance.com bijwerken om naar de CNAME te wijzen op onze WordPress-host, WPEngine. Het was hier een soort kip en ei - we hadden wat downtime voor https-URL's, wat zou mislukken zonder certificaat op de nieuwe site. Dit was oké omdat het certificaat op de oude site was verlopen, dus echt, we waren niet slechter af. Ik moest ook wachten om dit te doen totdat ik alle bestanden van de oude site had gedownload, want als DNS eenmaal was omgedraaid, was er geen manier om ze te bereiken, behalve via een achterdeur.

Terwijl ik wachtte tot DNS zich verspreidde, begon ik aan de logica te werken om alle vragen, antwoorden en opmerkingen in iets verbruiksartikelen in WordPress te verwerken. Niet alleen waren de tabelschema's anders dan die van WordPress, de soorten entiteiten zijn ook behoorlijk verschillend. Mijn visie was om elke vraag - en alle antwoorden en/of opmerkingen - te combineren in een enkele post.

Het lastige is dat de tabel met knooppunten alle drie de inhoudstypen in dezelfde tabel bevat, met ouderlijke en originele ("master") bovenliggende verwijzingen. Hun front-endcode gebruikt waarschijnlijk een soort cursor om door de inhoud te bladeren en deze in een hiërarchische en chronologische volgorde weer te geven. Ik zou die luxe niet hebben in WordPress, dus ik moest de HTML in één keer aan elkaar rijgen. Als voorbeeld, hier is hoe de gegevens eruit zagen:

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
*/

Ik kon niet bestellen op id, of type, of op ouder, omdat soms een opmerking later op een eerder antwoord kwam, het eerste antwoord niet altijd het geaccepteerde antwoord zou zijn, enzovoort. Ik wilde deze uitvoer (waar ++ staat voor één inspringingsniveau):

/*
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
*/

Ik begon een recursieve CTE te schrijven en, gedeeltelijk wegens teveel Rekorderlig die avond heb ik de hulp ingeroepen van collega Product Manager, Andy Mallon (@AMtwo). Hij hielp me deze vraag op te lossen, waardoor de berichten in de juiste volgorde zouden worden weergegeven (en je kunt dit fragment uitproberen, de ouders wijzigen en/of het geaccepteerde antwoord, om te zien dat de juiste volgorde nog steeds wordt geretourneerd):

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;

Resultaten:

/*
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     
*/

Genie. Ik zag een tiental anderen gecontroleerd en was blij dat ik door kon naar de volgende stap. Ik heb Andy verschillende keren uitgebreid bedankt, maar laat me het nog een keer doen:Bedankt Andy!

Nu ik de hele set in de gewenste volgorde kon retourneren, moest ik wat manipulatie van de uitvoer uitvoeren om HTML-elementen en klassenamen toe te passen waarmee ik vragen, antwoorden, opmerkingen en inspringingen op een zinvolle manier kon markeren. Het einddoel was een uitvoer die er als volgt uitzag (en onthoud, dit is een van de eenvoudigere gevallen):

<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>

Ik zal niet door het belachelijke aantal iteraties gaan dat ik moest doorlopen om op een betrouwbare vorm van die uitvoer te komen voor alle 5.000+ items (wat zich vertaalde in bijna 1.000 berichten zodra alles aan elkaar was gelijmd). Bovendien moest ik deze genereren in de vorm van INSERT uitspraken die ik vervolgens in phpMyAdmin op de WordPress-site kon plakken, wat betekende dat ik me aan hun bizarre syntaxisdiagram moest houden. Die verklaringen moesten andere aanvullende informatie bevatten die door WordPress wordt vereist, maar die niet aanwezig of nauwkeurig is in de brongegevens (zoals post_type ). En die beheerdersconsole zou een time-out krijgen als er te veel gegevens zijn, dus ik moest het opsplitsen in ~ 750 inserts per keer. Hier is de procedure waarmee ik eindigde (dit is niet echt om iets specifieks van te leren, alleen een demonstratie van hoeveel manipulatie van de geïmporteerde gegevens nodig was):

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

De uitvoer daarvan is nog niet compleet en nog niet klaar om in WordPress te vullen:

Voorbeelduitvoer (klik om te vergroten)

Ik zou wat extra hulp van C# nodig hebben om de daadwerkelijke inhoud (inclusief markdown) om te zetten in HTML en CSS die ik beter zou kunnen beheersen, en om de uitvoer te schrijven (een heleboel INSERT uitspraken die toevallig een heleboel HTML-code bevatten) naar bestanden op de schijf die ik kon openen en in phpMyAdmin plakken. Voor de HTML, platte tekst + markdown die als volgt begon:

Er is een [blogpost hier][1] die erover praat, en ook [dit bericht](https://ergens).

SELECTEER iets uit dbo.sometable;

[1]:https://elders

Zou dit moeten worden:

Er is een blogpost hier die erover praat, en ook dit bericht .

SELECTEER iets uit dbo.sometable;

Om dit voor elkaar te krijgen, heb ik de hulp ingeroepen van MarkdownSharp, een open source-bibliotheek van Stack Overflow die een groot deel van de markdown-naar-HTML-conversie afhandelt. Het paste goed bij mijn behoeften, maar niet perfect; Ik zou nog steeds verdere manipulatie moeten uitvoeren:

  • MarkdownSharp staat geen dingen toe zoals target=_blank , dus die zou ik na verwerking zelf moeten injecteren;
  • code (alles voorafgegaan door vier spaties) erft
    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();
              }
            }
          }
        }
      }
    }

    Ja, dat is een lelijke stapel code, maar het bracht me uiteindelijk bij de reeks uitvoer die phpMyAdmin niet zou laten kotsen, en dat WordPress mooi (genoeg) zou presenteren. Ik heb het C#-programma gewoon meerdere keren aangeroepen met de verschillende parameterbereiken:

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

    Daarna opende ik elk van de bestanden, plakte ze in phpMyAdmin en drukte op GO:

    phpMyAdmin (klik om te vergroten)

    Natuurlijk moest ik wat CSS toevoegen binnen WordPress om onderscheid te maken tussen vragen, opmerkingen en antwoorden, en ook om opmerkingen te laten inspringen om antwoorden op zowel vragen als antwoorden te tonen, opmerkingen die op opmerkingen reageren te nesten, enzovoort. Dit is hoe een fragment eruitziet als je doorklikt naar de vragen van een maand:

    Vraagtegel (klik om te vergroten)

    En dan een voorbeeldpost met ingesloten afbeeldingen, meerdere bijlagen, geneste opmerkingen en een antwoord:

    Voorbeeld van vraag en antwoord (klik om daarheen te gaan)

    Ik probeer nog steeds een paar berichten te herstellen die naar de site zijn verzonden nadat de laatste back-up is gemaakt, maar ik nodig u van harte uit om rond te bladeren. Laat het ons weten als u iets ziet dat ontbreekt of niet op zijn plaats is, of zelfs om ons te vertellen dat de inhoud nog steeds nuttig voor u is. We hopen de uploadfunctionaliteit voor abonnementen opnieuw te introduceren vanuit Plan Explorer, maar het vereist wat API-werk op de nieuwe ondersteuningssite, dus ik heb vandaag geen verwachte aankomsttijd voor je.

      Answers.SQLPerformance.com

  1. SQL-fout:ORA-02000:ontbreekt ALTIJD trefwoord bij het maken van een op identiteitskolom gebaseerde tabel

  2. SQL-query - resultaten samenvoegen tot één string

  3. Android Studio 3.0 kanarie 1:SQL-syntaxisfout

  4. Wat is het precies een BLOB in een DBMS-context?