001package gudusoft.gsqlparser.stmt.snowflake;
002
003
004import gudusoft.gsqlparser.EDbVendor;
005import gudusoft.gsqlparser.ESqlClause;
006import gudusoft.gsqlparser.ESqlStatementType;
007import gudusoft.gsqlparser.TCustomSqlStatement;
008import gudusoft.gsqlparser.ETableSource;
009import gudusoft.gsqlparser.nodes.TCreateTableOption;
010import gudusoft.gsqlparser.nodes.TObjectName;
011import gudusoft.gsqlparser.nodes.TObjectNameList;
012import gudusoft.gsqlparser.nodes.TParseTreeVisitor;
013import gudusoft.gsqlparser.nodes.TPathSqlNode;
014import gudusoft.gsqlparser.nodes.TStageLocation;
015import gudusoft.gsqlparser.nodes.TTable;
016import gudusoft.gsqlparser.nodes.snowflake.TCopyIntoNode;
017import gudusoft.gsqlparser.nodes.snowflake.TStageReference;
018import gudusoft.gsqlparser.stmt.TSelectSqlStatement;
019
020import java.util.ArrayList;
021
022public class TSnowflakeCopyIntoStmt extends TCustomSqlStatement {
023
024    private ArrayList<String> fileList = new ArrayList<>();
025
026
027    public ArrayList<String> getFileList() {
028        return fileList;
029    }
030
031    private String fileFormatName = null;
032    private String fileFormatType = null;
033
034    public void setFileFormatName(String fileFormatName) {
035        this.fileFormatName = fileFormatName;
036    }
037
038    public void setFileFormatType(String fileFormatType) {
039        this.fileFormatType = fileFormatType;
040    }
041
042    public String getFileFormatName() {
043        return fileFormatName;
044    }
045
046    public String getFileFormatType() {
047        return fileFormatType;
048    }
049
050    public static int COPY_INTO_TABLE = 0;
051    public static int COPY_INTO_LOCATION = 1;
052
053    private String regex_pattern;
054
055    public void setRegex_pattern(String regex_pattern) {
056        this.regex_pattern = regex_pattern;
057    }
058
059    public String getRegex_pattern() {
060        return regex_pattern;
061    }
062
063    private int copyIntoType = TSnowflakeCopyIntoStmt.COPY_INTO_TABLE;
064
065    public int getCopyIntoType() {
066        return copyIntoType;
067    }
068
069    private TStageLocation stageLocation;
070
071    public TStageLocation getStageLocation() {
072        return stageLocation;
073    }
074
075    private TObjectName tableName;
076    private TSelectSqlStatement subQuery;
077    public TSnowflakeCopyIntoStmt(EDbVendor dbvendor) {
078        super(dbvendor);
079        sqlstatementtype = ESqlStatementType.sstCopyInto;
080    }
081
082    public void setTableName(TObjectName tableName) {
083        this.tableName = tableName;
084    }
085
086    public void setSubQuery(TSelectSqlStatement subQuery) {
087        this.subQuery = subQuery;
088    }
089
090    public TObjectName getTableName() {
091
092        return tableName;
093    }
094
095    public TSelectSqlStatement getSubQuery() {
096        return subQuery;
097    }
098
099    private TObjectName fromSourceLocation;
100    public TObjectName getFromSourceLocation() {
101        return fromSourceLocation;
102    }
103
104    private TObjectNameList tableColumnList;
105
106    public void setTableColumnList(TObjectNameList tableColumnList) {
107        this.tableColumnList = tableColumnList;
108    }
109
110    public TObjectNameList getTableColumnList() {
111        return tableColumnList;
112    }
113
114
115    public int doParseStatement(TCustomSqlStatement psql) {
116        if (rootNode == null) return -1;
117        super.doParseStatement(psql);
118        TCopyIntoNode node = (TCopyIntoNode)(rootNode);
119        tableName = node.getTableName();
120        this.copyIntoType = node.getCopyIntoType();
121        this.stageLocation = node.getStageLocation();
122
123        if (node.getSubquery() != null){
124            subQuery = new TSelectSqlStatement(this.dbvendor);
125            subQuery.rootNode = node.getSubquery();
126            subQuery.doParseStatement(this);
127
128            // When the stage location is inside the subquery's FROM clause
129            // (e.g., COPY INTO t FROM (SELECT ... FROM @stage/path)),
130            // propagate it to the COPY INTO statement for API accessibility.
131            if (this.stageLocation == null && subQuery.getTables() != null) {
132                for (int i = 0; i < subQuery.getTables().size(); i++) {
133                    TTable table = subQuery.getTables().getTable(i);
134                    if (table.getTableType() == ETableSource.stageReference
135                            && table.getStageReference() != null) {
136                        TStageReference ref = table.getStageReference();
137                        this.stageLocation = new TStageLocation();
138                        this.stageLocation.setStage(true);
139                        if (ref.getStageName() != null) {
140                            String name = ref.getStageName().toString();
141                            if (name.startsWith("~")) {
142                                this.stageLocation.init(TStageLocation.EStageLocationType.internalUser);
143                            } else if (name.startsWith("%")) {
144                                this.stageLocation.init(TStageLocation.EStageLocationType.internalTable);
145                                TObjectName tblName = new TObjectName();
146                                tblName.setString(name.substring(1));
147                                this.stageLocation.setTableName(tblName);
148                            } else {
149                                this.stageLocation.init(TStageLocation.EStageLocationType.internalNamed, ref.getStageName());
150                            }
151                        }
152                        if (ref.getStagePath() != null) {
153                            this.stageLocation.setPath(ref.getStagePath());
154                        }
155                        break;
156                    }
157                }
158            }
159        }
160
161        ArrayList<TCreateTableOption> tableOptions = node.tableOptions;
162        if (tableOptions != null){
163            for(int i=0;i<tableOptions.size();i++){
164                tableOptions.get(i).doParse(this, ESqlClause.unknown);
165            }
166        }
167
168        fromSourceLocation = node.getFromSourceLocation();
169        tableColumnList = node.getTableColumnList();
170
171        // Mantis #4425: when the FROM clause uses a quoted string literal
172        // (e.g. FROM '@db.schema.stage/path' or FROM 's3://bucket/path'),
173        // the grammar stores it as fromSourceLocation rather than as a
174        // structured stage location, leaving stageLocation null.
175        // Parse the literal and populate stageLocation so the public API
176        // is consistent with the unquoted form.
177        if (this.stageLocation == null && fromSourceLocation != null) {
178            populateStageLocationFromLiteral(fromSourceLocation);
179        }
180
181        return 0;
182    }
183
184    private void populateStageLocationFromLiteral(TObjectName source) {
185        String raw = source.toString();
186        if (raw == null || raw.isEmpty()) return;
187
188        // Strip surrounding single quotes, if any.
189        String content = raw;
190        if (content.length() >= 2 && content.startsWith("'") && content.endsWith("'")) {
191            content = content.substring(1, content.length() - 1);
192        }
193        if (content.isEmpty()) return;
194
195        if (content.startsWith("@")) {
196            populateInternalStageLocation(content);
197        } else if (looksLikeExternalUrl(content)) {
198            TStageLocation loc = new TStageLocation();
199            loc.init(TStageLocation.EStageLocationType.location);
200            loc.setStage(false);
201            TObjectName extLoc = new TObjectName();
202            extLoc.setString(raw);
203            loc.setExternalLocation(extLoc);
204            this.stageLocation = loc;
205        }
206    }
207
208    private void populateInternalStageLocation(String content) {
209        // content begins with '@'
210        String afterAt = content.substring(1);
211
212        // Split into name part and path part at the first '/'.
213        String namePart = afterAt;
214        String pathPart = null;
215        int slashIdx = afterAt.indexOf('/');
216        if (slashIdx >= 0) {
217            namePart = afterAt.substring(0, slashIdx);
218            pathPart = afterAt.substring(slashIdx);
219        }
220
221        TStageLocation loc = new TStageLocation();
222        loc.setStage(true);
223
224        if (namePart.isEmpty() || namePart.equals("~") || namePart.startsWith("~")) {
225            // @~ or @~/path -> user stage
226            loc.init(TStageLocation.EStageLocationType.internalUser);
227        } else if (namePart.startsWith("%")) {
228            // @%table or @%table/path -> table stage
229            loc.init(TStageLocation.EStageLocationType.internalTable);
230            TObjectName tblName = new TObjectName();
231            tblName.setString(namePart.substring(1));
232            loc.setTableName(tblName);
233        } else {
234            // @stage, @schema.stage, or @db.schema.stage (with optional path)
235            TObjectName stageName = new TObjectName();
236            stageName.setString(namePart);
237            loc.init(TStageLocation.EStageLocationType.internalNamed, stageName);
238        }
239
240        if (pathPart != null && !pathPart.isEmpty()) {
241            TPathSqlNode path = new TPathSqlNode();
242            path.setString(pathPart);
243            loc.setPath(path);
244        }
245
246        this.stageLocation = loc;
247    }
248
249    private boolean looksLikeExternalUrl(String content) {
250        String lower = content.toLowerCase();
251        return lower.startsWith("s3://")
252            || lower.startsWith("azure://")
253            || lower.startsWith("gcs://")
254            || lower.startsWith("https://")
255            || lower.startsWith("http://")
256            || lower.startsWith("file://");
257    }
258
259    public void accept(TParseTreeVisitor v){
260        v.preVisit(this);
261        v.postVisit(this);
262    }
263
264    public void acceptChildren(TParseTreeVisitor v){
265        v.preVisit(this);
266        if (this.subQuery != null){
267            this.subQuery.acceptChildren(v);
268        }
269        v.postVisit(this);
270    }
271
272}