sql >> Databasteknik >  >> RDS >> Database

Migrera från AnswerHub till WordPress :A Tale of 10 Technologies

Vi lanserade nyligen en ny supportwebbplats där du kan ställa frågor, skicka produktfeedback eller funktionsförfrågningar eller öppna supportbiljetter. En del av målet var att centralisera alla platser där vi erbjöd hjälp till samhället. Detta inkluderade webbplatsen SQLPerformance.com Frågor och Svar, där Paul White, Hugo Kornelis och många andra har hjälpt till att lösa dina mest komplicerade frågor om justering av sökfrågor och exekveringsplaner, som går ända tillbaka till februari 2013. Jag berättar med blandade känslor att Frågor och svar-webbplatsen har stängts.

Det finns dock en uppsida. Du kan nu ställa de svåra frågorna på det nya supportforumet. Om du letar efter det gamla innehållet, ja, det finns fortfarande kvar, men det ser lite annorlunda ut. Av olika anledningar som jag inte kommer in på idag, när vi väl bestämde oss för att ta bort den ursprungliga Q&A-webbplatsen, bestämde vi oss till slut för att helt enkelt vara värd för allt befintligt innehåll på en skrivskyddad WordPress-webbplats, istället för att migrera det till baksidan. av den nya webbplatsen.

Det här inlägget handlar inte om skälen bakom beslutet.

Jag mådde riktigt dåligt över hur snabbt svarssidan var tvungen att bli offline, DNS bytte och innehållet migrerade. Eftersom en varningsbanner implementerades på sajten men AnswerHub faktiskt inte gjorde den synlig var detta en chock för många användare. Så jag ville se till att jag bevarade så mycket av innehållet som möjligt, och jag ville att det skulle vara rätt. Det här inlägget är här för att jag tyckte att det skulle vara intressant att prata om själva processen, hur många olika tekniker som var involverade i att få fram den, och att visa upp resultatet. Jag förväntar mig inte att någon av er kommer att dra nytta av detta från början, eftersom det här är en relativt obskyr migreringsväg, utan mer som ett exempel på att knyta ihop ett gäng tekniker för att utföra en uppgift. Det fungerar också som en bra påminnelse till mig själv om att många saker inte blir så lätta som de låter innan du börjar.

TL;DR är detta:Jag har lagt ner mycket tid och ansträngning på att få det arkiverade innehållet att se bra ut, även om jag fortfarande försöker återställa de senaste inläggen som kom in mot slutet. Jag använde dessa tekniker:

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

Därav titeln. Om du vill ha en stor del av de blodiga detaljerna, här är de. Om du har några frågor eller feedback, vänligen kontakta eller kommentera nedan.

AnswerHub tillhandahöll en 665 MB dumpfil från MySQL-databasen som var värd för Q&A-innehållet. Varje redigerare jag försökte kvävdes i det, så jag var först tvungen att dela upp det i en fil per tabell med det här praktiska Perl-skriptet från Jared Cheney. Tabellerna jag behövde hette network11_nodes (frågor, svar och kommentarer), network11_authoritables (användare) och network11_managed_files (alla bilagor, inklusive planuppladdningar):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 var de inte extremt snabba att ladda i SSMS, men där kunde jag åtminstone använda Ctrl +H för att ändra (till exempel) detta:

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

Till detta:

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

Sedan kunde jag ladda data till SQL Server så att jag kunde manipulera den. Och tro mig, jag manipulerade det.

Därefter var jag tvungen att hämta alla bilagor. Se, MySQL-dumpfilen jag fick från leverantören innehöll en gazillion INSERT uttalanden, men ingen av de faktiska planfilerna som användare hade laddat upp - databasen hade bara de relativa sökvägarna till filerna. Jag använde T-SQL för att bygga en serie PowerShell-kommandon som skulle anropa Invoke-WebRequest att hämta alla filer och lagra dem lokalt (många sätt att flå den här katten, men detta var helt enkelt). Från detta:

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

Det gav den här uppsättningen kommandon (tillsammans med ett förkommando för att lösa detta TLS-problem); det hela gick ganska snabbt, men jag rekommenderar inte detta tillvägagångssätt för någon kombination av {massive set of files} och/eller {low bandwidth}:

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

Detta laddade ner nästan alla bilagor, men vissa missades visserligen på grund av fel på den gamla sidan när de laddades upp först. Så på den nya webbplatsen kan du ibland se en referens till en bilaga som inte finns.

Sedan använde jag Panic Transmit 5 för att ladda upp temp mapp till den nya webbplatsen, och nu när innehållet laddas upp länkar det till /s/temp/1-proc.pesession kommer att fortsätta att fungera.

Därefter gick jag vidare till SSL. För att begära ett certifikat på den nya WordPress-webbplatsen var vi tvungna att uppdatera DNS för answers.sqlperformance.com för att peka på CNAME på vår WordPress-värd, WPEngine. Det var typ kyckling och ägg här - vi var tvungna att lida av lite driftstopp för https-webbadresser, vilket skulle misslyckas för inget certifikat på den nya webbplatsen. Det här var okej eftersom certifikatet på den gamla sajten hade gått ut, så egentligen hade vi det inte sämre. Jag var också tvungen att vänta med att göra detta tills jag hade laddat ner alla filer från den gamla webbplatsen, för när DNS väl vände över, skulle det inte finnas något sätt att komma åt dem förutom genom en bakdörr.

Medan jag väntade på att DNS skulle spridas började jag arbeta på logiken för att dra alla frågor, svar och kommentarer till något förbrukningsbart i WordPress. Inte bara skilde sig tabellschemana från WordPress, typerna av enheter är också ganska olika. Min vision var att kombinera varje fråga – och eventuella svar och/eller kommentarer – till ett enda inlägg.

Den knepiga delen är att nodtabellen bara innehåller alla de tre innehållstyperna i samma tabell, med överordnade och ursprungliga ("master") överordnade referenser. Deras front-end-kod använder sannolikt någon form av markör för att gå igenom och visa innehållet i hierarkisk och kronologisk ordning. Jag skulle inte ha den lyxen i WordPress, så jag var tvungen att sätta ihop HTML-koden i ett slag. Bara som ett exempel, så här såg data ut:

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

Jag kunde inte beställa efter id, typ, eller efter förälder, eftersom det ibland kom en kommentar senare på ett tidigare svar, det första svaret skulle inte alltid vara det accepterade svaret, och så vidare. Jag ville ha denna utdata (där ++ representerar en nivå av indrag):

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

Jag började skriva en rekursiv CTE och delvis på grund av för mycket Rekorderlig den kvällen tog jag hjälp av en kollega, Andy Mallon (@AMtwo). Han hjälpte mig att piska upp den här frågan, som skulle returnera inläggen i korrekt visningsordning (och du kan prova det här utdraget, ändra föräldrarna och/eller det accepterade svaret, för att se att rätt ordning fortfarande kommer att returneras):

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;

Resultat:

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

Geni. Jag kollade på ett dussintal andra och var glad över att gå vidare till nästa steg. Jag har tackat Andy ymnigt flera gånger, men låt mig göra det igen:Tack Andy!

Nu när jag kunde returnera hela uppsättningen i den ordning jag gillade, var jag tvungen att utföra viss manipulation av utdata för att tillämpa HTML-element och klassnamn som skulle låta mig markera frågor, svar, kommentarer och indrag på ett meningsfullt sätt. Slutmålet var utdata som såg ut så här (och kom ihåg att detta är ett av de enklare fallen):

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

Jag kommer inte att gå igenom det löjliga antal iterationer jag var tvungen att gå igenom för att landa på en tillförlitlig form av den utgången för alla 5 000+ artiklar (vilket översattes till nästan 1 000 inlägg när allt var ihopklistrat). Utöver det behövde jag generera dessa i form av INSERT uttalanden som jag sedan kunde klistra in i phpMyAdmin på WordPress-webbplatsen, vilket innebar att jag skulle följa deras bisarra syntaxdiagram. Dessa uttalanden behövde för att inkludera annan ytterligare information som krävs av WordPress, men som inte finns eller är korrekt i källdata (som post_type ). Och den administratörskonsolen skulle ta slut med för mycket data, så jag var tvungen att dela ut den i ~750 inlägg åt gången. Här är proceduren jag slutade med (detta är egentligen inte att lära sig något specifikt av, bara en demonstration av hur mycket manipulation av den importerade informationen som var nödvändig):

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

Utdata från det är inte komplett och inte redo att läggas in i WordPress ännu:

Exempel på utdata (klicka för att förstora)

Jag skulle behöva lite extra hjälp från C# för att omvandla det faktiska innehållet (inklusive markdown) till HTML och CSS som jag skulle kunna kontrollera bättre, och skriva utdata (ett gäng INSERT uttalanden som råkade innehålla en massa HTML-kod) till filer på disken som jag kunde öppna och klistra in i phpMyAdmin. För HTML, vanlig text + markdown som började så här:

Det finns ett [blogginlägg här][1] som talar om det, och även [det här inlägget](https://någonstans).

VÄLJ något från dbo.sometable;

[1]:https://annanstans

Skulle behöva bli detta:

Det finns ett blogginlägg här som talar om det, och även det här inlägget .

VÄLJ något från dbo.sometable;

För att få till det här tog jag hjälp av MarkdownSharp, ett bibliotek med öppen källkod som kommer från Stack Overflow som hanterar mycket av konverteringen från markdown-till-HTML. Det passade bra för mina behov, men inte perfekt; Jag skulle fortfarande behöva utföra ytterligare manipulation:

  • MarkdownSharp tillåter inte saker som target=_blank , så jag skulle behöva injicera dem själv efter bearbetning;
  • kod (allt med prefix med fyra blanksteg) ärver
    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, det är ett fult gäng kod, men det fick mig till slut till den uppsättning utdata som inte skulle få phpMyAdmin att spy, och som WordPress skulle presentera snyggt (tillräckligt). Jag ringde helt enkelt C#-programmet flera gånger med de olika parameterintervallen:

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

    Sedan öppnade jag var och en av filerna, klistrade in dem i phpMyAdmin och tryckte på GO:

    phpMyAdmin (klicka för att förstora)

    Naturligtvis var jag tvungen att lägga till lite CSS i WordPress för att hjälpa till att skilja mellan frågor, kommentarer och svar, och för att även indraga kommentarer för att visa svar på både frågor och svar, kapa kommentarer som svarar på kommentarer, och så vidare. Så här ser ett utdrag ut när du går igenom en månads frågor:

    Frågebricka (klicka för att förstora)

    Och sedan ett exempelinlägg som visar inbäddade bilder, flera bilagor, kapslade kommentarer och ett svar:

    Exempel på fråga och svar (klicka för att gå dit)

    Jag försöker fortfarande återställa några inlägg som skickades till webbplatsen efter att den senaste säkerhetskopian togs, men jag välkomnar dig att surfa runt. Meddela oss om du upptäcker att något saknas eller är fel, eller bara för att berätta att innehållet fortfarande är användbart för dig. Vi hoppas verkligen kunna återinföra planuppladdningsfunktioner från Plan Explorer, men det kommer att kräva lite API-arbete på den nya supportwebbplatsen, så jag har ingen ETA för dig idag.

      Answers.SQLPerformance.com

  1. Hur konverterar man tidsstämpel till datetime i MySQL?

  2. Interner i de sju SQL-serversorterna – Del 2

  3. Hur använder man GROUP BY för att sammanfoga strängar i MySQL?

  4. ORA-00918:kolumn är tvetydigt definierad i SELECT *