@naturalcycles/mysql-lib 3.8.0
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/CHANGELOG.md +198 -0
- package/dist/dbAdapter.d.ts +2 -0
- package/dist/dbAdapter.js +9 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +9 -0
- package/dist/mysql.db.d.ts +58 -0
- package/dist/mysql.db.js +214 -0
- package/dist/query.util.d.ts +11 -0
- package/dist/query.util.js +177 -0
- package/dist/schema/mysql.schema.util.d.ts +24 -0
- package/dist/schema/mysql.schema.util.js +109 -0
- package/package.json +52 -0
- package/readme.md +14 -0
- package/src/dbAdapter.ts +6 -0
- package/src/index.ts +7 -0
- package/src/mysql.db.ts +312 -0
- package/src/query.util.ts +216 -0
- package/src/schema/mysql.schema.util.ts +131 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { DBQuery } from '@naturalcycles/db-lib'
|
|
2
|
+
import { DBQueryFilterOperator } from '@naturalcycles/db-lib/src/query/dbQuery'
|
|
3
|
+
import { _hb } from '@naturalcycles/js-lib'
|
|
4
|
+
import { white, yellow } from '@naturalcycles/nodejs-lib/dist/colors'
|
|
5
|
+
import { QueryOptions } from 'mysql'
|
|
6
|
+
import * as mysql from 'mysql'
|
|
7
|
+
import { mapNameToMySQL } from './schema/mysql.schema.util'
|
|
8
|
+
|
|
9
|
+
const MAX_PACKET_SIZE = 1024 * 1024 // 1Mb
|
|
10
|
+
const MAX_ROW_SIZE = 800 * 1024 // 1Mb - margin
|
|
11
|
+
|
|
12
|
+
export function dbQueryToSQLSelect(q: DBQuery<any>): string {
|
|
13
|
+
const tokens = selectTokens(q)
|
|
14
|
+
|
|
15
|
+
// filters
|
|
16
|
+
tokens.push(...whereTokens(q))
|
|
17
|
+
|
|
18
|
+
// order
|
|
19
|
+
tokens.push(...orderTokens(q))
|
|
20
|
+
|
|
21
|
+
// offset/limit
|
|
22
|
+
tokens.push(...offsetLimitTokens(q))
|
|
23
|
+
|
|
24
|
+
return tokens.join(' ')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function dbQueryToSQLDelete(q: DBQuery<any>): string {
|
|
28
|
+
const tokens = [`DELETE FROM`, mysql.escapeId(q.table)]
|
|
29
|
+
|
|
30
|
+
// filters
|
|
31
|
+
tokens.push(...whereTokens(q))
|
|
32
|
+
|
|
33
|
+
// offset/limit
|
|
34
|
+
tokens.push(...offsetLimitTokens(q))
|
|
35
|
+
|
|
36
|
+
return tokens.join(' ')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns array of sql statements to respect the max sql size.
|
|
41
|
+
*/
|
|
42
|
+
export function insertSQL(
|
|
43
|
+
table: string,
|
|
44
|
+
records: Record<any, any>[],
|
|
45
|
+
verb: 'INSERT' | 'REPLACE' = 'INSERT',
|
|
46
|
+
): string[] {
|
|
47
|
+
// INSERT INTO table_name (column1, column2, column3, ...)
|
|
48
|
+
// VALUES (value1, value2, value3, ...);
|
|
49
|
+
|
|
50
|
+
// eslint-disable-next-line unicorn/no-array-reduce
|
|
51
|
+
const fieldSet = records.reduce((set: Set<string>, rec) => {
|
|
52
|
+
Object.keys(rec).forEach(field => set.add(field))
|
|
53
|
+
return set
|
|
54
|
+
}, new Set<string>())
|
|
55
|
+
const fields = [...fieldSet]
|
|
56
|
+
|
|
57
|
+
const start = [
|
|
58
|
+
verb,
|
|
59
|
+
`INTO`,
|
|
60
|
+
mysql.escapeId(table),
|
|
61
|
+
`(` + [...fields].map(f => mysql.escapeId(mapNameToMySQL(f))).join(',') + `)`,
|
|
62
|
+
`VALUES\n`,
|
|
63
|
+
].join(' ')
|
|
64
|
+
|
|
65
|
+
const valueRows = records.map(rec => {
|
|
66
|
+
return `(` + fields.map(k => mysql.escape(rec[k])).join(',') + `)`
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const full = start + valueRows.join(',\n')
|
|
70
|
+
|
|
71
|
+
if (full.length < MAX_PACKET_SIZE) return [full]
|
|
72
|
+
|
|
73
|
+
const sqls: string[] = []
|
|
74
|
+
let sql: string | undefined
|
|
75
|
+
|
|
76
|
+
valueRows.forEach(vrow => {
|
|
77
|
+
if (!sql) {
|
|
78
|
+
sql = start + vrow
|
|
79
|
+
} else {
|
|
80
|
+
if (sql.length + vrow.length >= MAX_ROW_SIZE) {
|
|
81
|
+
sqls.push(sql)
|
|
82
|
+
sql = start + vrow // reset
|
|
83
|
+
} else {
|
|
84
|
+
sql += ',\n ' + vrow // add
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if (sql) {
|
|
90
|
+
sqls.push(sql) // last one
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(
|
|
94
|
+
`${white(table)} large sql query (${yellow(_hb(full.length))}) was split into ${yellow(
|
|
95
|
+
sqls.length,
|
|
96
|
+
)} chunks`,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return sqls
|
|
100
|
+
|
|
101
|
+
// todo: handle "upsert" later
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function insertSQLSingle(table: string, record: Record<any, any>): string {
|
|
105
|
+
// INSERT INTO table_name (column1, column2, column3, ...)
|
|
106
|
+
// VALUES (value1, value2, value3, ...);
|
|
107
|
+
|
|
108
|
+
const tokens = [
|
|
109
|
+
`INSERT INTO`,
|
|
110
|
+
mysql.escapeId(table),
|
|
111
|
+
`(` +
|
|
112
|
+
Object.keys(record)
|
|
113
|
+
.map(f => mysql.escapeId(mapNameToMySQL(f)))
|
|
114
|
+
.join(',') +
|
|
115
|
+
`)`,
|
|
116
|
+
`\nVALUES`,
|
|
117
|
+
`(` +
|
|
118
|
+
Object.values(record)
|
|
119
|
+
.map(v => mysql.escapeId(v))
|
|
120
|
+
.join(',') +
|
|
121
|
+
`)`,
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
return tokens.join(' ')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function insertSQLSetSingle(table: string, record: Record<any, any>): QueryOptions {
|
|
128
|
+
return {
|
|
129
|
+
sql: `INSERT INTO ${mysql.escapeId(table)} SET ?`,
|
|
130
|
+
values: [record],
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function dbQueryToSQLUpdate(q: DBQuery<any>, record: Record<any, any>): string {
|
|
135
|
+
// var sql = mysql.format('UPDATE posts SET modified = ? WHERE id = ?', [CURRENT_TIMESTAMP, 42]);
|
|
136
|
+
const tokens = [
|
|
137
|
+
`UPDATE`,
|
|
138
|
+
mysql.escapeId(q.table),
|
|
139
|
+
`SET`,
|
|
140
|
+
Object.keys(record)
|
|
141
|
+
.map(f => mysql.escapeId(mapNameToMySQL(f)) + ' = ?')
|
|
142
|
+
.join(', '),
|
|
143
|
+
...whereTokens(q),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
return mysql.format(tokens.join(' '), Object.values(record))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function selectTokens(q: DBQuery<any>): string[] {
|
|
150
|
+
let fields = ['*']
|
|
151
|
+
|
|
152
|
+
if (q._selectedFieldNames) {
|
|
153
|
+
fields = q._selectedFieldNames.length ? q._selectedFieldNames : ['id']
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// We don't do `escapeId` cause it'll ruin e.g SELECT `count *` FROM ...
|
|
157
|
+
return [`SELECT`, fields.map(f => mapNameToMySQL(f)).join(', '), `FROM`, mysql.escapeId(q.table)]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function offsetLimitTokens(q: DBQuery<any>): string[] {
|
|
161
|
+
const tokens: string[] = []
|
|
162
|
+
|
|
163
|
+
if (q._limitValue) {
|
|
164
|
+
tokens.push(`LIMIT`, String(q._limitValue))
|
|
165
|
+
|
|
166
|
+
// In SQL OFFSET is only allowed if LIMIT is set
|
|
167
|
+
if (q._offsetValue) {
|
|
168
|
+
tokens.push(`OFFSET`, String(q._offsetValue))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return tokens
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function orderTokens(q: DBQuery<any>): string[] {
|
|
176
|
+
if (!q._orders.length) return []
|
|
177
|
+
return [
|
|
178
|
+
`ORDER BY`,
|
|
179
|
+
q._orders.map(o => `\`${o.name}\` ${o.descending ? 'DESC' : 'ASC'}`).join(', '),
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const OP_MAP: Partial<Record<DBQueryFilterOperator, string>> = {
|
|
184
|
+
'==': '=',
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function whereTokens(q: DBQuery<any>): string[] {
|
|
188
|
+
if (!q._filters.length) return []
|
|
189
|
+
|
|
190
|
+
return [
|
|
191
|
+
`WHERE`,
|
|
192
|
+
q._filters
|
|
193
|
+
.map(f => {
|
|
194
|
+
if (f.val === null || f.val === undefined) {
|
|
195
|
+
// special treatment
|
|
196
|
+
|
|
197
|
+
return [
|
|
198
|
+
mysql.escapeId(mapNameToMySQL(f.name)),
|
|
199
|
+
f.op === '==' ? 'IS NULL' : 'IS NOT NULL',
|
|
200
|
+
].join(' ')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (Array.isArray(f.val)) {
|
|
204
|
+
// special case for arrays
|
|
205
|
+
return `${mysql.escapeId(mapNameToMySQL(f.name))} IN (${mysql.escape(f.val)})`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return [
|
|
209
|
+
mysql.escapeId(mapNameToMySQL(f.name)),
|
|
210
|
+
OP_MAP[f.op] || f.op,
|
|
211
|
+
mysql.escape(f.val),
|
|
212
|
+
].join(' ')
|
|
213
|
+
})
|
|
214
|
+
.join(' AND '),
|
|
215
|
+
]
|
|
216
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JsonSchemaBoolean,
|
|
3
|
+
JsonSchemaNumber,
|
|
4
|
+
JsonSchemaObject,
|
|
5
|
+
JsonSchemaRootObject,
|
|
6
|
+
JsonSchemaString,
|
|
7
|
+
} from '@naturalcycles/js-lib'
|
|
8
|
+
import * as mysql from 'mysql'
|
|
9
|
+
|
|
10
|
+
export interface MySQLTableStats {
|
|
11
|
+
Field: string // created
|
|
12
|
+
Type: string // int(11)
|
|
13
|
+
Null: string // 'YES'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MySQLSchemaOptions {
|
|
17
|
+
/**
|
|
18
|
+
* @default 'InnoDB'
|
|
19
|
+
*/
|
|
20
|
+
engine?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* It currently skips nullability and declares everything as "DEFAULT NULL".
|
|
25
|
+
*/
|
|
26
|
+
export function jsonSchemaToMySQLDDL(
|
|
27
|
+
table: string,
|
|
28
|
+
schema: JsonSchemaObject<any>,
|
|
29
|
+
opt: MySQLSchemaOptions = {},
|
|
30
|
+
): string {
|
|
31
|
+
const { engine = 'InnoDB' } = opt
|
|
32
|
+
|
|
33
|
+
const lines: string[] = [`CREATE TABLE ${mysql.escapeId(table)} (`]
|
|
34
|
+
|
|
35
|
+
const innerLines = Object.entries(schema.properties).map(([k, s]) => {
|
|
36
|
+
if (k === 'id') {
|
|
37
|
+
return `id VARCHAR(255) NOT NULL`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let type
|
|
41
|
+
|
|
42
|
+
if (s.type === 'string') {
|
|
43
|
+
// can specify isoDate later
|
|
44
|
+
type = 'LONGTEXT'
|
|
45
|
+
} else if (s.type === 'integer') {
|
|
46
|
+
type = 'INT(11)'
|
|
47
|
+
} else if (s.type === 'number') {
|
|
48
|
+
if (['unixTimestamp', 'int32'].includes((s as JsonSchemaNumber).format!)) {
|
|
49
|
+
type = 'INT(11)'
|
|
50
|
+
} else {
|
|
51
|
+
type = 'FLOAT(11)'
|
|
52
|
+
}
|
|
53
|
+
} else if (s.type === 'boolean') {
|
|
54
|
+
type = 'TINYINT(1)'
|
|
55
|
+
} else if (s.instanceof === 'Buffer') {
|
|
56
|
+
type = 'LONGBLOB'
|
|
57
|
+
} else if (s.type === 'null') {
|
|
58
|
+
type = 'VARCHAR(255)'
|
|
59
|
+
} else if (s.type === 'array') {
|
|
60
|
+
type = 'LONGTEXT' // to be JSON.stringified?
|
|
61
|
+
} else if (s.type === 'object') {
|
|
62
|
+
type = 'LONGTEXT' // to be JSON.stringified?
|
|
63
|
+
} else {
|
|
64
|
+
// default
|
|
65
|
+
type = 'LONGTEXT'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tokens: string[] = [mysql.escapeId(mapNameToMySQL(k)), type, `DEFAULT NULL`]
|
|
69
|
+
|
|
70
|
+
return tokens.join(' ')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
innerLines.push(`PRIMARY KEY (id)`)
|
|
74
|
+
|
|
75
|
+
lines.push(innerLines.join(',\n'))
|
|
76
|
+
lines.push(`) ENGINE=${engine}`)
|
|
77
|
+
|
|
78
|
+
return lines.join('\n')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function mysqlTableStatsToJsonSchemaField<T = any>(
|
|
82
|
+
table: string,
|
|
83
|
+
stats: MySQLTableStats[],
|
|
84
|
+
): JsonSchemaRootObject<T> {
|
|
85
|
+
const s: JsonSchemaRootObject<T> = {
|
|
86
|
+
$id: `${table}.schema.json`,
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {} as any,
|
|
89
|
+
required: [],
|
|
90
|
+
additionalProperties: true,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
stats.forEach(stat => {
|
|
94
|
+
const name = stat.Field
|
|
95
|
+
const t = stat.Type.toLowerCase()
|
|
96
|
+
const notNull = stat.Null?.toUpperCase() !== 'YES'
|
|
97
|
+
if (notNull) {
|
|
98
|
+
s.required.push(name as any)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (t.includes('text') || t.includes('char')) {
|
|
102
|
+
s.properties[name] = { type: 'string' } as JsonSchemaString
|
|
103
|
+
} else if (t.includes('lob')) {
|
|
104
|
+
s.properties[name] = { instanceof: 'Buffer' }
|
|
105
|
+
} else if (t.startsWith('tinyint') || t.includes('(1)')) {
|
|
106
|
+
s.properties[name] = { type: 'boolean' } as JsonSchemaBoolean
|
|
107
|
+
} else if (t.startsWith('int(')) {
|
|
108
|
+
s.properties[name] = { type: 'integer' } as JsonSchemaNumber
|
|
109
|
+
} else if (t.startsWith('float')) {
|
|
110
|
+
s.properties[name] = { type: 'number' } as JsonSchemaNumber
|
|
111
|
+
} else {
|
|
112
|
+
console.log(s)
|
|
113
|
+
throw new Error(`Unknown mysql field type ${name} ${stat.Type}`)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return s
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Because MySQL doesn't support `.` in field names and escapes them as tableName + fieldName.
|
|
122
|
+
*
|
|
123
|
+
* @param name
|
|
124
|
+
*/
|
|
125
|
+
export function mapNameToMySQL(name: string): string {
|
|
126
|
+
return name.replace(/\./g, '_dot_')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function mapNameFromMySQL(name: string): string {
|
|
130
|
+
return name.replace(/_dot_/g, '.')
|
|
131
|
+
}
|