Repository: DerekStride/tree-sitter-sql (language: python)

Issue:
Support for COPY FROM STDIN
For example:

```sql
COPY some_table (a, b, c) FROM STDIN (FORMAT CSV);
1,2,3
d,e,f
\.
```

The actual data used in my situation is massive, and it causes the parsing to take a very long time (over a minute to open the file in neovim). I assume it would be much more efficient if the parser didn't attempt to parse the CSV data as SQL. 

I believe the ideal behavior would be to treat the data in between the `;` and `\.` as `csv` if the format is explicitly CSV, and text otherwise. Parsing other components of the `COPY` statement would be nice, but for me personally, handling the data contents would be the biggest win. 

Ref: https://www.postgresql.org/docs/current/sql-copy.html


Discussion / hints:
Hmm, ok, first we should parse this statement at all. :-) I can take a look
I have to ask. When checking the postgres documentation, there is no CSV part in the query. As the name suggests it is provided by stdin input. So the Parser would only parse everything before the ; anyways. Not sure how to handle that, because it is not part of the spec.

Fix patch:
diff --git a/grammar.js b/grammar.js
index 4e944a01..8ce538e7 100644
--- a/grammar.js
+++ b/grammar.js
@@ -249,6 +249,18 @@ module.exports = grammar({
     keyword_authorization: _ => make_keyword("authorization"),
     keyword_action: _ => make_keyword("action"),
     keyword_extension: _ => make_keyword("extension"),
+    keyword_copy: _ => make_keyword("copy"),
+    keyword_stdin: _ => make_keyword("stdin"),
+    keyword_freeze: _ => make_keyword("freeze"),
+    keyword_escape: _ => make_keyword("escape"),
+    keyword_encoding: _ => make_keyword("encoding"),
+    keyword_force_quote: _ => make_keyword("force_quote"),
+    keyword_quote: _ => make_keyword("quote"),
+    keyword_force_null: _ => make_keyword("force_null"),
+    keyword_force_not_null: _ => make_keyword("force_not_null"),
+    keyword_header: _ => make_keyword("header"),
+    keyword_match: _ => make_keyword("match"),
+    keyword_program: _ => make_keyword("program"),
 
     keyword_trigger: _ => make_keyword('trigger'),
     keyword_function: _ => make_keyword("function"),
@@ -336,6 +348,7 @@ module.exports = grammar({
     keyword_sort: _ => make_keyword("sort"),
     keyword_format: _ => make_keyword("format"),
     keyword_delimited: _ => make_keyword("delimited"),
+    keyword_delimiter: _ => make_keyword("delimiter"),
     keyword_fields: _ => make_keyword("fields"),
     keyword_terminated: _ => make_keyword("terminated"),
     keyword_escaped: _ => make_keyword("escaped"),
@@ -718,6 +731,7 @@ module.exports = grammar({
           $._insert_statement,
           $._update_statement,
           $._truncate_statement,
+          $._copy_statement,
         ),
       ),
     ),
@@ -2113,6 +2127,68 @@ module.exports = grammar({
       field('name', $.identifier),
     ),
 
+    _copy_statement: $ => seq(
+      $.keyword_copy,
+      $.object_reference,
+      $._column_list,
+      $.keyword_from,
+      choice(
+        $.keyword_stdin,
+        alias($._literal_string, "filename"),
+        seq($.keyword_program, alias($._literal_string, "command")),
+      ),
+      optional($.keyword_with),
+      wrapped_in_parenthesis(
+        repeat1(
+          choice(
+            seq(
+              $.keyword_format,
+              choice(
+                $.keyword_csv,
+                $.keyword_binary,
+                $.keyword_text,
+              ),
+            ),
+            seq(
+              $.keyword_freeze,
+              choice(
+                $.keyword_true,
+                $.keyword_false
+              )
+            ),
+            seq(
+              $.keyword_header,
+              choice(
+                $.keyword_true,
+                $.keyword_false,
+                $.keyword_match
+              ),
+            ),
+            seq(
+              choice(
+                $.keyword_delimiter,
+                $.keyword_null,
+                $.keyword_default,
+                $.keyword_escape,
+                $.keyword_quote,
+                $.keyword_encoding,
+              ),
+              alias($._literal_string, $.identifier)
+            ),
+            seq(
+              choice(
+                $.keyword_force_null,
+                $.keyword_force_not_null,
+                $.keyword_force_quote,
+              ),
+              $._column_list
+            ),
+          ),
+        ),
+      ),
+      optional($.where),
+    ),
+
     _insert_statement: $ => seq(
       $.insert,
       optional($.returning),
diff --git a/queries/highlights.scm b/queries/highlights.scm
index b902a0ed..2ed3bdd0 100644
--- a/queries/highlights.scm
+++ b/queries/highlights.scm
@@ -304,6 +304,19 @@
   (keyword_statement)
   (keyword_execute)
   (keyword_procedure)
+  (keyword_copy)
+  (keyword_delimiter)
+  (keyword_encoding)
+  (keyword_escape)
+  (keyword_force_not_null)
+  (keyword_force_null)
+  (keyword_force_quote)
+  (keyword_freeze)
+  (keyword_header)
+  (keyword_match)
+  (keyword_program)
+  (keyword_quote)
+  (keyword_stdin)
 ] @keyword
 
 [


Test patch:
diff --git a/test/corpus/insert.txt b/test/corpus/insert.txt
index 7d225b2f..2f297336 100644
--- a/test/corpus/insert.txt
+++ b/test/corpus/insert.txt
@@ -561,3 +561,95 @@ INSERT INTO some_table
           (select_expression
             (term
               (literal))))))))
+
+================================================================================
+COPY FROM STDIN CSV
+================================================================================
+
+COPY some_table (a, b, c) FROM STDIN (FORMAT CSV);
+
+--------------------------------------------------------------------------------
+(program
+  (statement
+    (keyword_copy)
+    (object_reference
+      (identifier))
+    (column
+      (identifier))
+    (column
+      (identifier))
+    (column
+      (identifier))
+    (keyword_from)
+    (keyword_stdin)
+    (keyword_format)
+    (keyword_csv)))
+
+================================================================================
+COPY FROM FILE
+================================================================================
+
+COPY some_table (a, b, c) FROM '/tmp/data.csv' (FORMAT CSV);
+
+--------------------------------------------------------------------------------
+
+(program
+  (statement
+    (keyword_copy)
+    (object_reference
+      (identifier))
+    (column
+      (identifier))
+    (column
+      (identifier))
+    (column
+      (identifier))
+    (keyword_from)
+    (keyword_format)
+    (keyword_csv)))
+
+================================================================================
+COPY FROM STDIN CSV WITH OPTIONS
+================================================================================
+
+COPY some_table (a, b, c) FROM STDIN WITH
+(
+    FORMAT CSV
+    DELIMITER ';'
+    QUOTE '"'
+    ESCAPE '\'
+    FREEZE true
+    HEADER MATCH
+    NULL 'NaN'
+);
+
+--------------------------------------------------------------------------------
+
+(program
+  (statement
+    (keyword_copy)
+    (object_reference
+      (identifier))
+    (column
+      (identifier))
+    (column
+      (identifier))
+    (column
+      (identifier))
+    (keyword_from)
+    (keyword_stdin)
+    (keyword_with)
+    (keyword_format)
+    (keyword_csv)
+    (keyword_delimiter)
+    (identifier)
+    (keyword_quote)
+    (identifier)
+    (keyword_escape)
+    (identifier)
+    (keyword_freeze)
+    (keyword_true)
+    (keyword_header)
+    (keyword_match)
+    (keyword_null)
+    (identifier)))