@rip-lang/schema 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1042 -0
- package/SCHEMA.md +1113 -0
- package/emit-sql.js +460 -0
- package/emit-types.js +366 -0
- package/generate.js +144 -0
- package/grammar.rip +504 -0
- package/index.js +39 -0
- package/lexer.js +438 -0
- package/orm.js +916 -0
- package/package.json +62 -0
- package/parser.js +246 -0
- package/runtime.js +494 -0
package/emit-sql.js
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// emit-sql.js - Generate SQL DDL from Schema AST
|
|
3
|
+
//
|
|
4
|
+
// Walks the S-expression AST produced by the schema parser and emits clean
|
|
5
|
+
// SQL DDL targeting DuckDB (CREATE TABLE, CREATE INDEX, CREATE TYPE).
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// import { generateSQL } from '@rip-lang/schema'
|
|
9
|
+
// const sql = generateSQL(ast)
|
|
10
|
+
//
|
|
11
|
+
// Author: Steve Shreeve <steve.shreeve@gmail.com>
|
|
12
|
+
// Date: February 2026
|
|
13
|
+
// ==============================================================================
|
|
14
|
+
|
|
15
|
+
// Schema type → SQL type mapping
|
|
16
|
+
const sqlTypeMap = {
|
|
17
|
+
string: 'VARCHAR',
|
|
18
|
+
text: 'TEXT',
|
|
19
|
+
integer: 'INTEGER',
|
|
20
|
+
number: 'DOUBLE',
|
|
21
|
+
boolean: 'BOOLEAN',
|
|
22
|
+
date: 'DATE',
|
|
23
|
+
datetime: 'TIMESTAMP',
|
|
24
|
+
email: 'VARCHAR',
|
|
25
|
+
url: 'VARCHAR',
|
|
26
|
+
uuid: 'UUID',
|
|
27
|
+
phone: 'VARCHAR',
|
|
28
|
+
json: 'JSON',
|
|
29
|
+
any: 'JSON',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Naming helpers
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
// Convert CamelCase to snake_case
|
|
37
|
+
function toSnakeCase(str) {
|
|
38
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Pluralization — ported from Rails ActiveSupport::Inflector
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const UNCOUNTABLES = new Set([
|
|
46
|
+
'equipment', 'information', 'rice', 'money', 'species',
|
|
47
|
+
'series', 'fish', 'sheep', 'jeans', 'police', 'data',
|
|
48
|
+
'feedback', 'metadata', 'media', 'aircraft', 'deer',
|
|
49
|
+
'moose', 'offspring', 'shrimp', 'swine', 'trout',
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
const IRREGULARS = new Map([
|
|
53
|
+
['person', 'people'],
|
|
54
|
+
['man', 'men'],
|
|
55
|
+
['woman', 'women'],
|
|
56
|
+
['child', 'children'],
|
|
57
|
+
['sex', 'sexes'],
|
|
58
|
+
['move', 'moves'],
|
|
59
|
+
['zombie', 'zombies'],
|
|
60
|
+
['goose', 'geese'],
|
|
61
|
+
['tooth', 'teeth'],
|
|
62
|
+
['foot', 'feet'],
|
|
63
|
+
['mouse', 'mice'],
|
|
64
|
+
['louse', 'lice'],
|
|
65
|
+
['ox', 'oxen'],
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
// Plural rules applied in reverse order (last match wins)
|
|
69
|
+
const PLURAL_RULES = [
|
|
70
|
+
[/$/, 's'],
|
|
71
|
+
[/s$/i, 's'],
|
|
72
|
+
[/^(ax|test)is$/i, '$1es'],
|
|
73
|
+
[/(octop|vir)us$/i, '$1i'],
|
|
74
|
+
[/(octop|vir)i$/i, '$1i'],
|
|
75
|
+
[/(alias|status)$/i, '$1es'],
|
|
76
|
+
[/(bu)s$/i, '$1ses'],
|
|
77
|
+
[/(buffal|tomat)o$/i, '$1oes'],
|
|
78
|
+
[/([ti])um$/i, '$1a'],
|
|
79
|
+
[/([ti])a$/i, '$1a'],
|
|
80
|
+
[/sis$/i, 'ses'],
|
|
81
|
+
[/(?:([^f])fe|([lr])f)$/i, '$1$2ves'],
|
|
82
|
+
[/(hive)$/i, '$1s'],
|
|
83
|
+
[/([^aeiouy]|qu)y$/i, '$1ies'],
|
|
84
|
+
[/(x|ch|ss|sh)$/i, '$1es'],
|
|
85
|
+
[/(matr|vert|ind)(?:ix|ex)$/i, '$1ices'],
|
|
86
|
+
[/(quiz)$/i, '$1zes'],
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
function pluralize(word) {
|
|
90
|
+
const lower = word.toLowerCase()
|
|
91
|
+
|
|
92
|
+
if (UNCOUNTABLES.has(lower)) return lower
|
|
93
|
+
if (IRREGULARS.has(lower)) return IRREGULARS.get(lower)
|
|
94
|
+
|
|
95
|
+
// Apply rules in reverse order (last rule wins)
|
|
96
|
+
for (let i = PLURAL_RULES.length - 1; i >= 0; i--) {
|
|
97
|
+
const [regex, replacement] = PLURAL_RULES[i]
|
|
98
|
+
if (regex.test(lower)) {
|
|
99
|
+
return lower.replace(regex, replacement)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return lower + 's'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Table and column naming
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
// Model name → table name: "User" → "users", "Person" → "people"
|
|
111
|
+
// Accepts optional overrides map: { Person: "people", Datum: "data" }
|
|
112
|
+
let _tableOverrides = {}
|
|
113
|
+
|
|
114
|
+
function toTableName(modelName) {
|
|
115
|
+
if (_tableOverrides[modelName]) return _tableOverrides[modelName]
|
|
116
|
+
return pluralize(toSnakeCase(modelName))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Foreign key: "User" → "user_id"
|
|
120
|
+
function toForeignKeyCol(name) {
|
|
121
|
+
return toSnakeCase(name) + '_id'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Index name: tableName + fields → "idx_users_role_status"
|
|
125
|
+
function toIndexName(tableName, fields) {
|
|
126
|
+
return 'idx_' + tableName + '_' + fields.map(f => toSnakeCase(f)).join('_')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// Dependency ordering
|
|
131
|
+
// =============================================================================
|
|
132
|
+
|
|
133
|
+
// Topological sort: models that are referenced via belongs_to come first
|
|
134
|
+
function topologicalSort(models) {
|
|
135
|
+
const byName = new Map()
|
|
136
|
+
for (const def of models) byName.set(def[1], def)
|
|
137
|
+
|
|
138
|
+
const deps = new Map()
|
|
139
|
+
for (const def of models) {
|
|
140
|
+
const name = def[1]
|
|
141
|
+
const body = def[3]
|
|
142
|
+
const refs = []
|
|
143
|
+
if (Array.isArray(body)) {
|
|
144
|
+
for (const member of body) {
|
|
145
|
+
if (Array.isArray(member) && member[0] === 'belongs_to' && byName.has(member[1])) {
|
|
146
|
+
refs.push(member[1])
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
deps.set(name, refs)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const sorted = []
|
|
154
|
+
const visited = new Set()
|
|
155
|
+
const visiting = new Set()
|
|
156
|
+
|
|
157
|
+
function visit(name) {
|
|
158
|
+
if (visited.has(name)) return
|
|
159
|
+
if (visiting.has(name)) return // circular ref — break cycle
|
|
160
|
+
visiting.add(name)
|
|
161
|
+
for (const dep of deps.get(name) || []) visit(dep)
|
|
162
|
+
visiting.delete(name)
|
|
163
|
+
visited.add(name)
|
|
164
|
+
sorted.push(byName.get(name))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const def of models) visit(def[1])
|
|
168
|
+
return sorted
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// =============================================================================
|
|
172
|
+
// SQL value formatting
|
|
173
|
+
// =============================================================================
|
|
174
|
+
|
|
175
|
+
function formatSQLDefault(val) {
|
|
176
|
+
if (val === true) return 'true'
|
|
177
|
+
if (val === false) return 'false'
|
|
178
|
+
if (val === null) return 'NULL'
|
|
179
|
+
if (typeof val === 'number') return String(val)
|
|
180
|
+
if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`
|
|
181
|
+
if (Array.isArray(val)) {
|
|
182
|
+
if (val[0] === 'object') return "'{}'"
|
|
183
|
+
if (val[0] === 'array') return "'[]'"
|
|
184
|
+
}
|
|
185
|
+
return `'${String(val).replace(/'/g, "''")}'`
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// =============================================================================
|
|
189
|
+
// Column type resolution
|
|
190
|
+
// =============================================================================
|
|
191
|
+
|
|
192
|
+
function resolveSQLType(type, constraints, enums) {
|
|
193
|
+
// Array types → JSON (DuckDB supports JSON arrays natively)
|
|
194
|
+
if (Array.isArray(type) && type[0] === 'array') {
|
|
195
|
+
return 'JSON'
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const base = sqlTypeMap[type]
|
|
199
|
+
if (base) {
|
|
200
|
+
// VARCHAR with max length from constraints
|
|
201
|
+
if ((base === 'VARCHAR') && constraints && constraints.length >= 2) {
|
|
202
|
+
return `VARCHAR(${constraints[1]})`
|
|
203
|
+
}
|
|
204
|
+
return base
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Enum reference
|
|
208
|
+
if (enums.has(type)) {
|
|
209
|
+
return toSnakeCase(type)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Nested type reference → store as JSON
|
|
213
|
+
return 'JSON'
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// =============================================================================
|
|
217
|
+
// Enum emission
|
|
218
|
+
// =============================================================================
|
|
219
|
+
|
|
220
|
+
function emitEnumSQL(def) {
|
|
221
|
+
const [, name, values] = def
|
|
222
|
+
const typeName = toSnakeCase(name)
|
|
223
|
+
let members
|
|
224
|
+
|
|
225
|
+
if (Array.isArray(values[0])) {
|
|
226
|
+
// Valued enum: use the member names as SQL enum values
|
|
227
|
+
members = values.map(([member]) => `'${member}'`)
|
|
228
|
+
} else {
|
|
229
|
+
// Simple enum
|
|
230
|
+
members = values.map(v => `'${v}'`)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return `CREATE TYPE ${typeName} AS ENUM (${members.join(', ')});`
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// =============================================================================
|
|
237
|
+
// Table emission
|
|
238
|
+
// =============================================================================
|
|
239
|
+
|
|
240
|
+
function emitTableSQL(def, enums) {
|
|
241
|
+
const [, name, parent, body] = def
|
|
242
|
+
const tableName = toTableName(name)
|
|
243
|
+
const columns = []
|
|
244
|
+
const indexes = []
|
|
245
|
+
|
|
246
|
+
// Auto-generated primary key
|
|
247
|
+
columns.push(' id UUID PRIMARY KEY DEFAULT gen_random_uuid()')
|
|
248
|
+
|
|
249
|
+
if (Array.isArray(body)) {
|
|
250
|
+
for (const member of body) {
|
|
251
|
+
if (!Array.isArray(member)) continue
|
|
252
|
+
const kind = member[0]
|
|
253
|
+
|
|
254
|
+
if (kind === 'field') {
|
|
255
|
+
const col = emitColumnSQL(member, enums)
|
|
256
|
+
if (col) columns.push(col)
|
|
257
|
+
|
|
258
|
+
} else if (kind === 'timestamps') {
|
|
259
|
+
columns.push(' created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP')
|
|
260
|
+
columns.push(' updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP')
|
|
261
|
+
|
|
262
|
+
} else if (kind === 'softDelete') {
|
|
263
|
+
columns.push(' deleted_at TIMESTAMP')
|
|
264
|
+
|
|
265
|
+
} else if (kind === 'belongs_to') {
|
|
266
|
+
const [, target, opts] = member
|
|
267
|
+
const fkCol = toForeignKeyCol(target)
|
|
268
|
+
const refTable = toTableName(target)
|
|
269
|
+
const notNull = isRelationOptional(opts) ? '' : ' NOT NULL'
|
|
270
|
+
columns.push(` ${fkCol} UUID${notNull} REFERENCES ${refTable}(id)`)
|
|
271
|
+
|
|
272
|
+
} else if (kind === 'index') {
|
|
273
|
+
const [, fields, unique] = member
|
|
274
|
+
const sqlFields = fields.map(f => toSnakeCase(f))
|
|
275
|
+
const idxName = toIndexName(tableName, fields)
|
|
276
|
+
const uniqueStr = unique ? 'UNIQUE ' : ''
|
|
277
|
+
indexes.push(`CREATE ${uniqueStr}INDEX ${idxName} ON ${tableName} (${sqlFields.join(', ')});`)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const lines = [`CREATE TABLE ${tableName} (`]
|
|
283
|
+
lines.push(columns.join(',\n'))
|
|
284
|
+
lines.push(');')
|
|
285
|
+
|
|
286
|
+
const result = [lines.join('\n')]
|
|
287
|
+
if (indexes.length > 0) {
|
|
288
|
+
result.push(indexes.join('\n'))
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return result.join('\n\n')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function emitColumnSQL(field, enums) {
|
|
295
|
+
const [, name, modifiers, type, fieldConstraints] = field
|
|
296
|
+
const colName = toSnakeCase(name)
|
|
297
|
+
const sqlType = resolveSQLType(type, fieldConstraints, enums)
|
|
298
|
+
const parts = [` ${colName} ${sqlType}`]
|
|
299
|
+
|
|
300
|
+
// NOT NULL for required fields
|
|
301
|
+
if (modifiers?.includes('!')) {
|
|
302
|
+
parts.push('NOT NULL')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// UNIQUE for # modifier
|
|
306
|
+
if (modifiers?.includes('#')) {
|
|
307
|
+
parts.push('UNIQUE')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// DEFAULT value
|
|
311
|
+
if (fieldConstraints && fieldConstraints.length === 1) {
|
|
312
|
+
parts.push(`DEFAULT ${formatSQLDefault(fieldConstraints[0])}`)
|
|
313
|
+
} else if (fieldConstraints && fieldConstraints.length >= 3) {
|
|
314
|
+
parts.push(`DEFAULT ${formatSQLDefault(fieldConstraints[2])}`)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return parts.join(' ')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function isRelationOptional(opts) {
|
|
321
|
+
if (!Array.isArray(opts) || opts[0] !== 'object') return false
|
|
322
|
+
for (let i = 1; i < opts.length; i++) {
|
|
323
|
+
if (Array.isArray(opts[i]) && opts[i][0] === 'optional' && opts[i][1] === true) {
|
|
324
|
+
return true
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return false
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// =============================================================================
|
|
331
|
+
// Links table (universal temporal associations)
|
|
332
|
+
// =============================================================================
|
|
333
|
+
|
|
334
|
+
function hasLinks(models) {
|
|
335
|
+
for (const def of models) {
|
|
336
|
+
const body = def[3]
|
|
337
|
+
if (!Array.isArray(body)) continue
|
|
338
|
+
for (const member of body) {
|
|
339
|
+
if (Array.isArray(member) && member[0] === 'link') return true
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return false
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function emitLinksTableSQL() {
|
|
346
|
+
const lines = [
|
|
347
|
+
'CREATE TABLE links (',
|
|
348
|
+
' id UUID PRIMARY KEY DEFAULT gen_random_uuid(),',
|
|
349
|
+
' source_type VARCHAR NOT NULL,',
|
|
350
|
+
' source_id UUID NOT NULL,',
|
|
351
|
+
' target_type VARCHAR NOT NULL,',
|
|
352
|
+
' target_id UUID NOT NULL,',
|
|
353
|
+
' role VARCHAR NOT NULL,',
|
|
354
|
+
' when_from TIMESTAMP,',
|
|
355
|
+
' when_till TIMESTAMP,',
|
|
356
|
+
' created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
|
|
357
|
+
');',
|
|
358
|
+
'',
|
|
359
|
+
'CREATE INDEX idx_links_source ON links (source_type, source_id);',
|
|
360
|
+
'CREATE INDEX idx_links_target ON links (target_type, target_id);',
|
|
361
|
+
'CREATE INDEX idx_links_role ON links (role);',
|
|
362
|
+
]
|
|
363
|
+
return lines.join('\n')
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// =============================================================================
|
|
367
|
+
// Main entry point
|
|
368
|
+
// =============================================================================
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Generate SQL DDL from a schema AST.
|
|
372
|
+
*
|
|
373
|
+
* @param {Array} ast - The S-expression AST from parse()
|
|
374
|
+
* @param {Object} [options] - Generation options
|
|
375
|
+
* @param {boolean} [options.enums=true] - Emit CREATE TYPE for enums
|
|
376
|
+
* @param {boolean} [options.tables=true] - Emit CREATE TABLE for models
|
|
377
|
+
* @param {boolean} [options.dropFirst=false] - Prepend DROP TABLE IF EXISTS
|
|
378
|
+
* @param {Object} [options.tableNames] - Override table names: { Person: "people" }
|
|
379
|
+
* @param {string} [options.header] - Custom header comment
|
|
380
|
+
* @returns {string} SQL DDL source
|
|
381
|
+
*/
|
|
382
|
+
export function generateSQL(ast, options = {}) {
|
|
383
|
+
if (!Array.isArray(ast) || ast[0] !== 'schema') {
|
|
384
|
+
throw new Error('Invalid schema AST: expected ["schema", ...]')
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const {
|
|
388
|
+
enums: emitEnums = true,
|
|
389
|
+
tables: emitTables = true,
|
|
390
|
+
dropFirst = false,
|
|
391
|
+
tableNames = {},
|
|
392
|
+
header = '-- Generated by @rip-lang/schema — do not edit\n',
|
|
393
|
+
} = options
|
|
394
|
+
|
|
395
|
+
// Set table name overrides for this generation run
|
|
396
|
+
_tableOverrides = tableNames
|
|
397
|
+
|
|
398
|
+
// Collect enum names for type resolution
|
|
399
|
+
const enumNames = new Set()
|
|
400
|
+
for (let i = 1; i < ast.length; i++) {
|
|
401
|
+
if (Array.isArray(ast[i]) && ast[i][0] === 'enum') {
|
|
402
|
+
enumNames.add(ast[i][1])
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const blocks = []
|
|
407
|
+
if (header) blocks.push(header)
|
|
408
|
+
|
|
409
|
+
// Enums first (tables reference them)
|
|
410
|
+
if (emitEnums) {
|
|
411
|
+
for (let i = 1; i < ast.length; i++) {
|
|
412
|
+
const def = ast[i]
|
|
413
|
+
if (Array.isArray(def) && def[0] === 'enum') {
|
|
414
|
+
blocks.push(emitEnumSQL(def))
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Tables
|
|
420
|
+
if (emitTables) {
|
|
421
|
+
// Optionally drop existing tables
|
|
422
|
+
if (dropFirst) {
|
|
423
|
+
const drops = []
|
|
424
|
+
for (let i = 1; i < ast.length; i++) {
|
|
425
|
+
const def = ast[i]
|
|
426
|
+
if (Array.isArray(def) && def[0] === 'model') {
|
|
427
|
+
drops.push(`DROP TABLE IF EXISTS ${toTableName(def[1])} CASCADE;`)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (drops.length > 0) {
|
|
431
|
+
blocks.push(drops.join('\n'))
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Collect models and sort by dependency order (referenced tables first)
|
|
436
|
+
const models = []
|
|
437
|
+
for (let i = 1; i < ast.length; i++) {
|
|
438
|
+
const def = ast[i]
|
|
439
|
+
if (Array.isArray(def) && def[0] === 'model') models.push(def)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const sorted = topologicalSort(models)
|
|
443
|
+
for (const def of sorted) {
|
|
444
|
+
blocks.push(emitTableSQL(def, enumNames))
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Auto-generate links table if any model uses @link
|
|
448
|
+
if (hasLinks(models)) {
|
|
449
|
+
if (dropFirst) {
|
|
450
|
+
blocks.push('DROP TABLE IF EXISTS links CASCADE;')
|
|
451
|
+
}
|
|
452
|
+
blocks.push(emitLinksTableSQL())
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return blocks.join('\n\n') + '\n'
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export { toSnakeCase, toTableName, pluralize }
|
|
460
|
+
export default generateSQL
|