-
Notifications
You must be signed in to change notification settings - Fork 39
/
parser.js
269 lines (234 loc) · 7.46 KB
/
parser.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
const NearleyParser = require('nearley').Parser;
const NearleyGrammar = require('nearley').Grammar;
const MySQLGrammarRules = require('./mysql/parser/grammar');
const MySQLCompactFormatter = require('./mysql/formatter/compact');
const MySQLJSONSchemaFormatter = require('./mysql/formatter/json-schema');
const { JSONSchemaFormatOptions, JSONSchemaFileOptions } = require('./shared/options');
const utils = require('./shared/utils');
const fs = require('fs');
const { join } = require('path');
/**
* Main Parser class, wraps nearley parser main methods.
*/
class Parser {
/**
* Parser constructor.
* Default dialect is 'mysql'.
*
* @param {string} dialect SQL dialect ('mysql' or 'mariadb' currently supported).
*/
constructor(dialect = 'mysql') {
if (!dialect || dialect === 'mysql' || dialect === 'mariadb') {
this.compiledGrammar = NearleyGrammar.fromCompiled(MySQLGrammarRules);
this.compactFormatter = MySQLCompactFormatter;
this.jsonSchemaFormatter = MySQLJSONSchemaFormatter;
}
else {
throw new TypeError(`Unsupported SQL dialect given to parser: '${dialect}. ` +
`Please provide 'mysql', 'mariadb' or none to use default.`);
}
this.resetParser();
/**
* Parsed statements.
* @type {string[]}
*/
this.statements = [];
/**
* Remains of string feed, after last parsed statement.
*/
this.remains = '';
/**
* Whether preparser is currently escaped.
*/
this.escaped = false;
/**
* Current quote char of preparser.
*/
this.quoted = '';
}
/**
* Feed chunk of string into parser.
*
* @param {string} chunk Chunk of string to be parsed.
* @returns {Parser} Parser class.
*/
feed(chunk) {
let i, char, parsed = '';
let lastStatementIndex = 0;
for (i = 0; i < chunk.length; i++) {
char = chunk[i];
parsed += char;
if (char === '\\') {
this.escaped = !this.escaped;
} else {
if (!this.escaped && this.isQuoteChar(char)) {
if (this.quoted) {
if (this.quoted === char) {
this.quoted = '';
}
} else {
this.quoted = char;
}
} else if (char === ';' && !this.quoted) {
const statement = this.remains + parsed.substr(lastStatementIndex, i + 1);
this.statements.push(statement);
this.remains = '';
lastStatementIndex = i + 1;
}
this.escaped = false;
}
}
this.remains += parsed.substr(lastStatementIndex);
return this;
}
/**
* Recreates NearleyParser using grammar given in constructor.
*
* @returns {void}
*/
resetParser() {
this.parser = new NearleyParser(this.compiledGrammar);
}
/**
* Checks whether character is a quotation character.
*
* @param {string} char Character to be evaluated.
* @returns {boolean} Whether char is quotation char.
*/
isQuoteChar(char) {
return char === '"' || char === "'" || char === '`';
}
/**
* Tidy parser results.
*
* @param {any} results Parser results.
* @returns {any} Tidy results.
*/
tidy(results) {
return results[0];
}
/**
* Parser results getter. Will run nearley parser on string fed to this parser.
*
* @returns {any} Parsed results.
*/
get results() {
let lineCount = 1;
let statement = this.statements.shift();
const results = [];
/**
* Since we separate the statements, if there is a parse error in a block among
* several statements in a stream, the parser will throw the error with line
* 0 counting from the beginning of the statement, not the stream. So we
* need to catch and correct line count incrementally along the stream.
*
* https://github.com/duartealexf/sql-ddl-to-json-schema/issues/20
*/
try {
while (statement) {
this.parser.feed(statement);
lineCount += (statement.match(/\r\n|\r|\n/g) || []).length;
results.push(this.tidy(this.parser.results));
statement = this.statements.shift();
this.resetParser();
}
} catch (e) {
/**
* Apply line count correction.
*/
if (e.message && utils.isString(e.message)) {
const matches = e.message.match(/^invalid syntax at line (\d+)/);
if (matches && Array.isArray(matches) && matches.length > 1) {
const errorLine = Number(matches[1]);
const newCount = lineCount + errorLine - 1;
e.message = e.message.replace(/\d+/, newCount);
}
}
/**
* Reset everything to not affect next feed.
*/
this.resetParser();
this.statements = [];
this.remains = '';
this.escaped = false;
this.quoted = '';
throw e;
}
/**
* Reset remains to not affect next feed.
*/
this.remains = '';
this.escaped = false;
this.quoted = '';
return {
id: "MAIN",
def: results
};
}
/**
* Formats given parsed JSON to a compact format.
* If no JSON is given, will use currently parsed SQL.
*
* @param {any} json Parsed JSON format (optional).
* @returns {any[]} Array of tables in compact JSON format.
*/
toCompactJson(json = null) {
if (!json) {
json = this.results;
}
return this.compactFormatter.format(json);
}
/**
* Formats parsed SQL to an array of JSON Schema documents,
* where each item is the JSON Schema of a table. If no
* tables are given, will use currently parsed SQL.
*
* @param {JSONSchemaFormatOptions} options Options available to format as JSON Schema (optional).
* @param {any[]} tables Array of tables in compact JSON format (optional).
* @returns {any[]} JSON Schema documents array.
*/
toJsonSchemaArray(options = new JSONSchemaFormatOptions(), tables = null) {
if (!tables) {
tables = this.toCompactJson();
}
return this.jsonSchemaFormatter.format(tables, options);
}
/**
* Output JSON Schema files (one for each table) in given output directory for the
* parsed SQL. If no JSON Schemas array is given, will use currently parsed SQL.
*
* @param {string} outputDir Output directory.
* @param {JSONSchemaFileOptions} options Options object for JSON Schema output to files (optional).
* @param {any[]} jsonSchemas JSON Schema documents array (optional).
* @returns {Promise<string[]>} Resolved promise with output file paths.
*/
toJsonSchemaFiles(outputDir, options = new JSONSchemaFileOptions(), jsonSchemas = null) {
if (!outputDir) {
throw new Error('Please provide output directory for JSON Schema files');
}
if (!jsonSchemas) {
jsonSchemas = this.toJsonSchemaArray({
useRef: options.useRef,
});
}
return Promise.all(
jsonSchemas.map(schema => {
return new Promise(resolve => {
if (!schema.$id) {
throw new Error('No root $id found in schema. It should contain the table name. ' +
'If you have modified the JSON Schema, please keep the $id, as it will be the file name.');
}
const filename = schema.$id;
const filepath = join(outputDir, filename + options.extension);
fs.writeFile(filepath, JSON.stringify(schema, null, options.indent), err => {
if (err) {
throw new Error(`Error when trying to write to file ${filepath}: ${JSON.stringify(err, null, 2)}`);
}
resolve(filepath);
});
});
})
);
}
}
module.exports = Parser;