{"id":8093,"date":"2026-04-03T15:34:02","date_gmt":"2026-04-03T07:34:02","guid":{"rendered":"https:\/\/kyle.ai\/blog\/?p=8093"},"modified":"2026-04-03T21:02:36","modified_gmt":"2026-04-03T13:02:36","slug":"golang-%e5%ae%9e%e7%8e%b0%e5%88%86%e5%89%b2-sql-%e5%ad%97%e7%ac%a6%e4%b8%b2","status":"publish","type":"post","link":"https:\/\/kyle.ai\/blog\/8093.html","title":{"rendered":"Golang \u5b9e\u73b0\u5206\u5272 SQL \u5b57\u7b26\u4e32"},"content":{"rendered":"\n<p>\u5728\u5f00\u53d1\u4e2d\u7684\u4f7f\u7528\u573a\u666f\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u6709\u4e00\u4e2a SQL \u6587\u4ef6\uff0c\u5b58\u653e\u4e86\u975e\u5e38\u591a\u7684 SQL \u8bed\u53e5\uff0c\u9700\u8981\u7a0b\u5e8f\u5c06\u5176\u5206\u5272\u6210\u4e00\u4e2a\u4e00\u4e2a\u7684 SQL \u8bed\u53e5\uff0c\u7136\u540e\u5355\u72ec\u4e00\u6761\u4e00\u6761\u7684\u5904\u7406\uff1b<\/li>\n\n\n\n<li>\u5728\u81ea\u5df1\u5b9e\u73b0 Flink \u6216\u5176\u5b83\u4efb\u52a1\u6267\u884c\u5f15\u64ce\u6846\u67b6\u65f6\uff0c\u7528\u6237\u628a\u60f3\u8981\u6309\u987a\u5e8f\u6267\u884c\u7684 SQL \u5199\u5230\u4e00\u4e2a\u6587\u4ef6\u4e2d\uff0c\u5f15\u64ce\u6267\u884c\u65f6\uff0c\u9700\u8981\u628a\u8fd9\u4e2a\u6587\u4ef6\u4e2d\u7684 SQL \u5206\u5272\u6210\u4e00\u6761\u6761\u5355\u72ec\u8fd0\u884c\u7684 SQL \u5b50\u8bed\u53e5\u3002<\/li>\n<\/ul>\n\n\n\n<p>\u8981\u89e3\u51b3\u4ee5\u4e0a\u95ee\u9898\uff0c\u9700\u8981\u81ea\u5df1\u5199\u4e00\u4e2a\u5206\u5272 SQL \u8bed\u53e5\u7684\u7b97\u6cd5\u3002<\/p>\n\n\n\n<p>\u53ef\u4ee5\u4f7f\u7528\u7f51\u4e0a\u5f00\u6e90\u7684\u5e93\u6765\u5b9e\u73b0\uff0c\u4f46\u662f\u8fc7\u4e8e\u81c3\u80bf\u3002\u4e8e\u662f\u81ea\u5df1\u7528 Golang \u5b9e\u73b0\u4e86\u4e00\u4e2a\u3002<\/p>\n\n\n\n<p>\u5206\u5272 SQL \u8bed\u53e5\u4e3b\u8981\u901a\u8fc7\u5206\u53f7 ; \u5206\u5272\u7b26\uff0c\u4f46\u662f\u8981\u8003\u8651\u5230\u8fd9\u4e2a\u5206\u53f7\uff0c\u662f\u6709\u53ef\u80fd\u5728\u5b57\u7b26\u4e32\u4e2d\u7684\uff0c\u4f8b\u5982<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nSELECT a FROM tt WHERE name=&quot;kyle;dust;&quot;; SELECT b FROM c WHERE id=1;\n<\/pre><\/div>\n\n\n<p>\u8fd8\u8981\u8003\u8651\u5230 Flink SQL \u4e2d\uff0cSTATEMENT SET \u7684\u8bed\u6cd5\uff0c\u4e5f\u5c31\u662f\u6279\u91cf\u6267\u884c\u591a\u6761 SQL\uff0c\u4f8b\u5982<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nEXECUTE STATEMENT SET\nBEGIN\nINSERT INTO portrait_feature\nSELECT  uid,\n        Array&#x5B;\n            Row(&#039;location_country&#039;, CAST(location_country AS STRING), &#039;STRING&#039;)\n            ] as data\nFROM X;\nEND;\n\nBEGIN STATEMENT SET;\nINSERT INTO portrait_feature SELECT a FROM b;\nINSERT INTO portrait_feature SELECT a FROM b;\nEND;\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\">\u6e90\u7801<\/h2>\n\n\n\n<p>\u4ee3\u7801\u91cf\u4e0d\u591a\uff0c\u53ea\u7528\u4e00\u4e2a\u6587\u4ef6\u641e\u5b9a sqlsplit.go<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: go; title: ; notranslate\" title=\"\">\npackage sqlsplit\n\nimport (\n\t&quot;strings&quot;\n\t&quot;unicode\/utf8&quot;\n)\n\ntype SplitConfig struct {\n\tDialect        string\n\tStripSemicolon bool\n}\n\ntype splitOption func(*SplitConfig)\n\nfunc WithDialect(dialect string) splitOption {\n\treturn func(c *SplitConfig) {\n\t\tc.Dialect = dialect\n\t}\n}\n\nfunc WithStripSemicolon() splitOption {\n\treturn func(c *SplitConfig) {\n\t\tc.StripSemicolon = true\n\t}\n}\n\n\/\/ Split \u5c06 sql \u5206\u5272\u6210\u591a\u4e2a\u72ec\u7acb\u7684 sql\n\/\/\ntype Split struct {\n\t\/\/ src \u662f\u6e90 sql \u5b57\u7b26\u4e32\n\tsrc string\n\t\/\/ the current position of the cursor\n\tcursor int\n\t\/\/ the start position of the current statement\n\tstart int\n\t\/\/ \u4fdd\u5b58\u6700\u8fd1\u770b\u5230\u7684\u6807\u8bc6\u6807\n\tidents *seenIdentifiers\n\t\/\/ \u662f\u5426\u8fdb\u5165\u4e86 statement set \u591a\u8bed\u53e5\u4e2d\n\tstmtSet bool\n\tconfig  *SplitConfig\n}\n\n\/\/ New \u751f\u6210\u4e00\u4e2a Split \u5b9e\u4f8b\nfunc New(input string, opts ...splitOption) *Split {\n\tspliter := &amp;Split{\n\t\tsrc:    input,\n\t\tidents: newSeenIdentifiers(2),\n\t\tconfig: &amp;SplitConfig{},\n\t}\n\tfor _, opt := range opts {\n\t\topt(spliter.config)\n\t}\n\treturn spliter\n}\n\nfunc (s *Split) Split() &#x5B;]string {\n\tvar statements &#x5B;]string\n\tfor {\n\t\tstatement := s.scan()\n\t\tif statement == &quot;&quot; {\n\t\t\tbreak\n\t\t}\n\t\tstatements = append(statements, statement)\n\t}\n\treturn statements\n}\n\nfunc (s *Split) stmt() string {\n\t_ = s.next()\n\n\t\/\/ \u7a7a\u767d\u7b26\u548c\u5355\u884c\u6ce8\u91ca\n\t\/\/ \u6362\u884c\u7b26\u4e0d\u88ab\u5f53\u4f5c\u7a7a\u767d\u7b26\u5904\u7406\n\tfor {\n\t\tch := s.peek()\n\t\tif isWhitespace(ch) {\n\t\t\ts.scanWhitespace()\n\t\t\tcontinue\n\t\t}\n\t\tif isSingleLineComment(ch, s.lookAhead(1)) {\n\t\t\ts.scanSingleLineComment()\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\n\tstmt := s.src&#x5B;s.start:s.cursor]\n\ts.start = s.cursor\n\tstmt = strings.TrimSpace(stmt)\n\tif s.config.StripSemicolon {\n\t\tstmt = strings.TrimSuffix(stmt, &quot;;&quot;)\n\t}\n\tstmt = strings.TrimSpace(stmt)\n\treturn stmt\n}\n\nfunc (s *Split) scanIdentifier() {\n\tcurPos := s.cursor\n\tfor {\n\t\tch := s.peek()\n\t\tif isLetter(ch) || isNumber(ch) || ch == &#039;_&#039; {\n\t\t\ts.next()\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\tidentifier := s.src&#x5B;curPos:s.cursor]\n\t\/\/ \u5224\u65ad flink \u7684 statement set \u8bed\u53e5\n\tif strings.ToUpper(identifier) == &quot;SET&quot; &amp;&amp; strings.ToUpper(s.idents.current()) == &quot;STATEMENT&quot; {\n\t\ts.stmtSet = true\n\t}\n\ts.idents.add(identifier)\n}\n\n\/\/ \u9047\u5230\u5206\u53f7\u65f6\u8c03\u7528\uff0c\u5982\u679c\u5f53\u524d\u5728 statement set \u4e2d\uff0c\u5219\u7ee7\u7eed\u626b\u63cf\uff0c\u76f4\u5230\u9047\u5230 END \u5173\u952e\u5b57\nfunc (s *Split) tryStmt() (string, bool) {\n\tif s.stmtSet {\n\t\tif s.idents.current() == &quot;END&quot; {\n\t\t\ts.stmtSet = false\n\t\t\treturn s.stmt(), true\n\t\t}\n\t\treturn &quot;&quot;, false\n\t}\n\treturn s.stmt(), true\n}\n\n\/\/ scan scans the next statement and returns it.\nfunc (s *Split) scan() string {\n\tfor {\n\t\tch := s.peek()\n\t\tswitch {\n\t\tcase isEOF(ch):\n\t\t\treturn s.stmt()\n\t\tcase ch == &#039;;&#039;:\n\t\t\tstmt, ok := s.tryStmt()\n\t\t\tif ok {\n\t\t\t\treturn stmt\n\t\t\t}\n\t\t\t_ = s.next()\n\t\tcase isSingleQuote(ch):\n\t\t\ts.scanString(&#039;\\&#039;&#039;)\n\t\tcase isDoubleQuote(ch):\n\t\t\ts.scanString(&#039;&quot;&#039;)\n\t\tcase isLetter(ch):\n\t\t\ts.scanIdentifier()\n\t\tcase isSingleLineComment(ch, s.lookAhead(1)):\n\t\t\ts.scanSingleLineComment()\n\t\tcase isMultiLineComment(ch, s.lookAhead(1)):\n\t\t\ts.scanMultiLineComment()\n\t\tdefault:\n\t\t\t_ = s.next()\n\t\t}\n\t}\n}\n\nfunc (s *Split) scanString(quote rune) {\n\tch := s.next() \/\/ consume the opening quote\n\tescaped := false\n\n\tfor {\n\t\tif escaped {\n\t\t\t\/\/ encountered an escape character\n\t\t\t\/\/ reset the escaped flag and continue\n\t\t\tescaped = false\n\t\t\tch = s.next()\n\t\t\tcontinue\n\t\t}\n\n\t\tif ch == &#039;\\\\&#039; {\n\t\t\tescaped = true\n\t\t\tch = s.next()\n\t\t\tcontinue\n\t\t}\n\n\t\tif ch == quote {\n\t\t\ts.next() \/\/ consume the closing quote\n\t\t\treturn\n\t\t}\n\n\t\tif isEOF(ch) {\n\t\t\t\/\/ encountered EOF before closing quote\n\t\t\t\/\/ this usually happens when the string is truncated\n\t\t\treturn\n\t\t}\n\t\tch = s.next()\n\t}\n}\n\nfunc (s *Split) scanWhitespace() {\n\t\/\/ scan whitespace, tab, carriage return\n\tch := s.next()\n\tfor isWhitespace(ch) {\n\t\tch = s.next()\n\t}\n}\n\nfunc (s *Split) scanSingleLineComment() {\n\tch := s.nextBy(2) \/\/ consume the opening dashes\n\tfor ch != &#039;\\n&#039; &amp;&amp; !isEOF(ch) {\n\t\tch = s.next()\n\t}\n\t\/\/ \u628a\u6362\u884c\u7b26\u4e5f\u653e\u5230 comment \u4e2d\n\t\/\/ \u4e3a\u4e86\u4e0e sqlparse \u5e93\u903b\u8f91\u4fdd\u6301\u4e00\u81f4\n\tif ch == &#039;\\n&#039; {\n\t\t_ = s.next()\n\t}\n}\n\nfunc (s *Split) scanMultiLineComment() {\n\tch := s.nextBy(2) \/\/ consume the opening slash and asterisk\n\tfor {\n\t\tif ch == &#039;*&#039; &amp;&amp; s.lookAhead(1) == &#039;\/&#039; {\n\t\t\ts.nextBy(2) \/\/ consume the closing asterisk and slash\n\t\t\tbreak\n\t\t}\n\t\tif isEOF(ch) {\n\t\t\t\/\/ encountered EOF before closing comment\n\t\t\t\/\/ this usually happens when the comment is truncated\n\t\t\treturn\n\t\t}\n\t\tch = s.next()\n\t}\n}\n\n\/\/ lookAhead returns the rune n positions ahead of the cursor.\nfunc (s *Split) lookAhead(n int) rune {\n\tif s.cursor+n &gt;= len(s.src) || s.cursor+n &lt; 0 {\n\t\treturn 0\n\t}\n\tr, _ := utf8.DecodeRuneInString(s.src&#x5B;s.cursor+n:])\n\treturn r\n}\n\n\/\/ peek returns the rune at the cursor position.\nfunc (s *Split) peek() rune {\n\treturn s.lookAhead(0)\n}\n\n\/\/ nextBy advances the cursor by n positions and returns the rune at the cursor position.\nfunc (s *Split) nextBy(n int) rune {\n\t\/\/ advance the cursor by n and return the rune at the cursor position\n\tif s.cursor+n &gt; len(s.src) {\n\t\treturn 0\n\t}\n\ts.cursor += n\n\tif s.cursor &gt;= len(s.src) {\n\t\treturn 0\n\t}\n\tr, _ := utf8.DecodeRuneInString(s.src&#x5B;s.cursor:])\n\treturn r\n}\n\n\/\/ next advances the cursor by 1 position and returns the rune at the cursor position.\nfunc (s *Split) next() rune {\n\treturn s.nextBy(1)\n}\n\nfunc isEOF(ch rune) bool {\n\treturn ch == 0\n}\n\nfunc isSingleLineComment(ch rune, nextCh rune) bool {\n\treturn ch == &#039;-&#039; &amp;&amp; nextCh == &#039;-&#039;\n}\n\nfunc isMultiLineComment(ch rune, nextCh rune) bool {\n\treturn ch == &#039;\/&#039; &amp;&amp; nextCh == &#039;*&#039;\n}\n\nfunc isSingleQuote(ch rune) bool {\n\treturn ch == &#039;\\&#039;&#039;\n}\n\nfunc isDoubleQuote(ch rune) bool {\n\treturn ch == &#039;&quot;&#039;\n}\n\nfunc isLetter(ch rune) bool {\n\treturn (ch &gt;= &#039;a&#039; &amp;&amp; ch &lt;= &#039;z&#039;) || (ch &gt;= &#039;A&#039; &amp;&amp; ch &lt;= &#039;Z&#039;)\n}\n\nfunc isNumber(ch rune) bool {\n\treturn ch &gt;= &#039;0&#039; &amp;&amp; ch &lt;= &#039;9&#039;\n}\n\n\/\/ \u975e\u6362\u884c\u7b26\u7684\u5176\u5b83\u7a7a\u767d\u7b26\nfunc isWhitespace(ch rune) bool {\n\treturn ch == &#039; &#039; || ch == &#039;\\t&#039; || ch == &#039;\\r&#039;\n}\n\ntype seenIdentifiers struct {\n\tsize        int\n\tidentifiers &#x5B;]string\n\tpos         int\n}\n\nfunc newSeenIdentifiers(size int) *seenIdentifiers {\n\treturn &amp;seenIdentifiers{\n\t\tsize:        size,\n\t\tidentifiers: make(&#x5B;]string, size),\n\t\tpos:         0,\n\t}\n}\n\nfunc (s *seenIdentifiers) add(identifier string) {\n\tif s.size == 0 {\n\t\treturn\n\t}\n\ts.identifiers&#x5B;s.pos] = identifier\n\ts.pos = (s.pos + 1) % s.size\n}\n\nfunc (s *seenIdentifiers) current() string {\n\tif s.size == 0 {\n\t\treturn &quot;&quot;\n\t}\n\treturn s.identifiers&#x5B;(s.pos-1+s.size)%s.size]\n}\n\nfunc (s *seenIdentifiers) String() string {\n\tif s.size == 0 || s.identifiers&#x5B;0] == &quot;&quot; {\n\t\treturn &quot;&quot;\n\t}\n\n\tordered := make(&#x5B;]string, 0, s.size)\n\tif s.identifiers&#x5B;s.pos] == &quot;&quot; {\n\t\tordered = append(ordered, s.identifiers&#x5B;:s.pos]...)\n\t} else {\n\t\tfor i := 0; i &lt; s.size; i++ {\n\t\t\tidx := (s.pos + i) % s.size\n\t\t\tordered = append(ordered, s.identifiers&#x5B;idx])\n\t\t}\n\t}\n\n\treturn strings.Join(ordered, &quot; &quot;)\n}\n\n<\/pre><\/div>\n\n\n<p>\u4e5f\u653e\u4e86\u4e00\u4efd\u4ee3\u7801\u5230 Github \u4e0a\uff1a<a href=\"https:\/\/gist.github.com\/kylege\/88c87d1697dda64662280107e7f853e9\">https:\/\/gist.github.com\/kylege\/88c87d1697dda64662280107e7f853e9<\/a><\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u5728\u5f00\u53d1\u4e2d\u7684\u4f7f\u7528\u573a\u666f\uff1a \u8981\u89e3\u51b3\u4ee5\u4e0a\u95ee\u9898\uff0c\u9700\u8981\u81ea\u5df1\u5199\u4e00\u4e2a\u5206\u5272 SQL \u8bed\u53e5\u7684\u7b97\u6cd5\u3002 \u53ef\u4ee5\u4f7f\u7528\u7f51\u4e0a\u5f00\u6e90\u7684\u5e93\u6765\u5b9e\u73b0\uff0c\u4f46 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[],"class_list":["post-8093","post","type-post","status-publish","format-standard","hentry","category-diary"],"_links":{"self":[{"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/posts\/8093","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/comments?post=8093"}],"version-history":[{"count":4,"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/posts\/8093\/revisions"}],"predecessor-version":[{"id":8102,"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/posts\/8093\/revisions\/8102"}],"wp:attachment":[{"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/media?parent=8093"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/categories?post=8093"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/tags?post=8093"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}