@nuxodin/item 0.6.4 → 0.6.7
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/package.json +1 -1
- package/tools/schema/db/mysql/from-db.js +3 -3
- package/tools/schema/db/mysql/from-field.js +10 -1
- package/tools/schema/db/mysql/to-db.js +12 -25
- package/tools/schema/db/pg/from-field.js +6 -9
- package/tools/schema/db/pg/to-db.js +3 -14
- package/tools/schema/db/shared/needs-ddl.js +28 -0
- package/tools/schema/db/sqlite/from-db.js +10 -6
- package/tools/schema/db/sqlite/from-field.js +19 -12
- package/tools/schema/db/sqlite/to-db.js +11 -6
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// mysql/from-db.js
|
|
2
2
|
import { schemaFromDb as _schemaFromDb } from '../shared/from-db.js'
|
|
3
|
-
import { quoteId } from '../shared/sql.js'
|
|
3
|
+
import { queryRows, quoteId } from '../shared/sql.js'
|
|
4
4
|
import { schemaFromField } from './from-field.js'
|
|
5
5
|
|
|
6
6
|
const dialect = {
|
|
7
|
-
tables: async (query) => (await query
|
|
8
|
-
fields: async (query, table) => query
|
|
7
|
+
tables: async (query) => (await queryRows(query, 'SHOW TABLES')).map(r => Object.values(r)[0]),
|
|
8
|
+
fields: async (query, table) => queryRows(query, `SHOW FULL FIELDS FROM ${quoteId(table)}`),
|
|
9
9
|
fromField: (row) => ({
|
|
10
10
|
name: row.Field,
|
|
11
11
|
prop: schemaFromField(row),
|
|
@@ -90,7 +90,7 @@ export function schemaFromField(row) {
|
|
|
90
90
|
};
|
|
91
91
|
} else prop.type = "string";
|
|
92
92
|
|
|
93
|
-
if (row.Default != null) prop.default = row.Default;
|
|
93
|
+
if (row.Default != null) prop.default = parseDefault(row.Default, prop.type);
|
|
94
94
|
if (row.Comment) prop["$comment"] = row.Comment;
|
|
95
95
|
if (row.Key === "PRI") prop["x-index"] = "primary";
|
|
96
96
|
if (row.Key === "UNI") prop["x-index"] = "unique";
|
|
@@ -99,3 +99,12 @@ export function schemaFromField(row) {
|
|
|
99
99
|
|
|
100
100
|
return prop;
|
|
101
101
|
}
|
|
102
|
+
|
|
103
|
+
/** SHOW FULL FIELDS hands the default back as text — read it as the value the schema would declare. */
|
|
104
|
+
function parseDefault(value, type) {
|
|
105
|
+
const v = String(value);
|
|
106
|
+
const n = Number(v);
|
|
107
|
+
if (type === "boolean") return v !== "0";
|
|
108
|
+
if ((type === "integer" || type === "number") && !isNaN(n)) return n;
|
|
109
|
+
return v;
|
|
110
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { schemaFromDb } from "./from-db.js";
|
|
3
3
|
import { quoteId, toFieldDef } from "./to-field.js";
|
|
4
4
|
import { schemaDiff } from "../../diff.js";
|
|
5
|
+
import { fieldNeedsDdl } from "../shared/needs-ddl.js";
|
|
5
6
|
|
|
6
7
|
function tableData(tableSchema) {
|
|
7
8
|
const row = tableSchema.additionalProperties ?? {};
|
|
@@ -14,26 +15,10 @@ function primaryFields(fields) {
|
|
|
14
15
|
return fields.filter(([, f]) => f["x-index"] === "primary").map(([n]) => n);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const columnProps = new Set([
|
|
18
|
-
"type", "format", "contentEncoding", "maxLength", "minLength",
|
|
19
|
-
"minimum", "maximum", "multipleOf", "$comment",
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
18
|
function hasOwn(obj, key) {
|
|
23
19
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
function fieldChanged(diffs, table, field, prop) {
|
|
27
|
-
return diffs.some(d =>
|
|
28
|
-
d.path[0] === "properties" &&
|
|
29
|
-
d.path[1] === table &&
|
|
30
|
-
d.path[2] === "additionalProperties" &&
|
|
31
|
-
d.path[3] === "properties" &&
|
|
32
|
-
d.path[4] === field &&
|
|
33
|
-
(columnProps.has(d.property) || d.property === "default" && hasOwn(prop, "default"))
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
22
|
function preserveDefault(prop, currProp) {
|
|
38
23
|
return hasOwn(prop, "default") || !hasOwn(currProp, "default")
|
|
39
24
|
? prop
|
|
@@ -153,7 +138,8 @@ export async function schemaToDb(
|
|
|
153
138
|
const { fields: currFieldEntries, required: currRequired } = tableData(
|
|
154
139
|
current.properties[table],
|
|
155
140
|
);
|
|
156
|
-
const currFields =
|
|
141
|
+
const currFields = new Map(currFieldEntries);
|
|
142
|
+
const nextFieldNames = new Set(fields.map(([n]) => n));
|
|
157
143
|
const indexStmts = await indexStatements(
|
|
158
144
|
query,
|
|
159
145
|
table,
|
|
@@ -165,20 +151,21 @@ export async function schemaToDb(
|
|
|
165
151
|
const autoAfter = new Map();
|
|
166
152
|
for (const [name, prop] of fields) {
|
|
167
153
|
const isRequired = required.has(name);
|
|
168
|
-
const
|
|
169
|
-
const propWithDefault =
|
|
154
|
+
const currProp = currFields.get(name);
|
|
155
|
+
const propWithDefault = currProp ? preserveDefault(prop, currProp) : prop;
|
|
170
156
|
const fieldProp = propWithDefault["x-autoincrement"] ? { ...propWithDefault, "x-autoincrement": false } : propWithDefault;
|
|
171
157
|
const fieldDef = toFieldDef(name, fieldProp, { required: isRequired });
|
|
172
|
-
if (!
|
|
158
|
+
if (!currProp) {
|
|
173
159
|
stmts.push(`ALTER TABLE ${tableId} ADD COLUMN ${fieldDef};`);
|
|
174
160
|
if (prop["x-autoincrement"]) autoAfter.set(name, prop);
|
|
175
161
|
} else {
|
|
176
|
-
const hasAuto = !!
|
|
162
|
+
const hasAuto = !!currProp["x-autoincrement"];
|
|
177
163
|
const wantsAuto = !!prop["x-autoincrement"];
|
|
178
|
-
|
|
164
|
+
// AUTO_INCREMENT is added and dropped by its own statements below, so it stays out of here
|
|
165
|
+
const wantsChange = fieldNeedsDdl(fieldProp, { ...currProp, "x-autoincrement": false }, ["multipleOf", "$comment"]) ||
|
|
179
166
|
isRequired !== currRequired.has(name) ||
|
|
180
167
|
hasAuto && !wantsAuto;
|
|
181
|
-
const changed = wantsChange && fieldDef !== toFieldDef(name,
|
|
168
|
+
const changed = wantsChange && fieldDef !== toFieldDef(name, currProp, { required: currRequired.has(name) });
|
|
182
169
|
if (changed) {
|
|
183
170
|
stmts.push(`ALTER TABLE ${tableId} MODIFY COLUMN ${fieldDef};`);
|
|
184
171
|
}
|
|
@@ -186,8 +173,8 @@ export async function schemaToDb(
|
|
|
186
173
|
}
|
|
187
174
|
}
|
|
188
175
|
if (!patch) {
|
|
189
|
-
for (const name of currFields) {
|
|
190
|
-
if (!
|
|
176
|
+
for (const name of currFields.keys()) {
|
|
177
|
+
if (!nextFieldNames.has(name)) {
|
|
191
178
|
stmts.push(`ALTER TABLE ${tableId} DROP COLUMN ${quoteId(name)};`);
|
|
192
179
|
}
|
|
193
180
|
}
|
|
@@ -46,7 +46,7 @@ export function schemaFromField(row) {
|
|
|
46
46
|
} else if (type === 'json' || type === 'jsonb') prop.type = 'object'
|
|
47
47
|
else prop.type = 'string'
|
|
48
48
|
|
|
49
|
-
const def =
|
|
49
|
+
const def = parseDefault(row.column_default, prop.type)
|
|
50
50
|
if (def !== undefined) prop.default = def
|
|
51
51
|
if (row.comment) prop['$comment'] = row.comment
|
|
52
52
|
if (row.is_primary) prop['x-index'] = 'primary'
|
|
@@ -56,16 +56,13 @@ export function schemaFromField(row) {
|
|
|
56
56
|
return prop
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
/** information_schema hands the default back as an SQL literal — read it as the value the schema would declare. */
|
|
60
|
+
function parseDefault(value, type) {
|
|
60
61
|
if (value == null || /^nextval\(/i.test(value) || /\b(current_|now\()/i.test(value)) return undefined
|
|
61
62
|
let v = String(value).replace(/::[\w\s.[\]"]+$/i, '')
|
|
62
63
|
if (/^'.*'$/.test(v)) v = v.slice(1, -1).replace(/''/g, "'")
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
if (
|
|
64
|
+
const n = Number(v)
|
|
65
|
+
if (type === 'boolean') return /^(true|t|1)$/i.test(v)
|
|
66
|
+
if ((type === 'integer' || type === 'number') && !isNaN(n)) return n
|
|
66
67
|
return v
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
-
function numberOr(fallback, value) {
|
|
70
|
-
return Number.isNaN(value) ? fallback : value
|
|
71
|
-
}
|
|
@@ -3,6 +3,7 @@ import { schemaFromDb } from './from-db.js'
|
|
|
3
3
|
import { queryRows } from '../shared/sql.js'
|
|
4
4
|
import { defaultLiteral, quoteId, quoteLit, toFieldDef, typeSql } from './to-field.js'
|
|
5
5
|
import { schemaDiff } from '../../diff.js'
|
|
6
|
+
import { fieldNeedsDdl } from '../shared/needs-ddl.js'
|
|
6
7
|
|
|
7
8
|
function tableData(tableSchema) {
|
|
8
9
|
const row = tableSchema.additionalProperties ?? {}
|
|
@@ -15,11 +16,6 @@ function primaryFields(fields) {
|
|
|
15
16
|
return fields.filter(([, f]) => f['x-index'] === 'primary').map(([n]) => n)
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const columnProps = new Set([
|
|
19
|
-
'type', 'format', 'contentEncoding', 'maxLength', 'minLength',
|
|
20
|
-
'minimum', 'maximum', 'multipleOf',
|
|
21
|
-
])
|
|
22
|
-
|
|
23
19
|
function hasOwn(obj, key) {
|
|
24
20
|
return Object.prototype.hasOwnProperty.call(obj, key)
|
|
25
21
|
}
|
|
@@ -30,14 +26,6 @@ function setEqual(a, b) {
|
|
|
30
26
|
return true
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
function fieldChanged(diffs, table, field, prop) {
|
|
34
|
-
return diffs.some(d => {
|
|
35
|
-
const [a, b, c, e, f] = d.path
|
|
36
|
-
return a === 'properties' && b === table && c === 'additionalProperties' && e === 'properties' && f === field &&
|
|
37
|
-
(columnProps.has(d.property) || d.property === 'default' && hasOwn(prop, 'default') || d.property === 'x-autoincrement')
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
|
|
41
29
|
function preserveDefault(prop, currProp) {
|
|
42
30
|
return hasOwn(prop, 'default') || !hasOwn(currProp, 'default')
|
|
43
31
|
? prop
|
|
@@ -163,7 +151,8 @@ export async function schemaToDb(schema, query, { force = false, patch = false }
|
|
|
163
151
|
continue
|
|
164
152
|
}
|
|
165
153
|
const propWithDefault = preserveDefault(prop, currProp)
|
|
166
|
-
const changed =
|
|
154
|
+
const changed = fieldNeedsDdl(propWithDefault, currProp, ['multipleOf', '$comment']) ||
|
|
155
|
+
isRequired !== currRequired.has(name)
|
|
167
156
|
if (changed) stmts.push(...alterFieldStmts(table, name, propWithDefault, currProp, isRequired, currRequired.has(name), changed))
|
|
168
157
|
}
|
|
169
158
|
if (!patch) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// shared/needs-ddl.js — does the existing column still satisfy the schema?
|
|
2
|
+
//
|
|
3
|
+
// The schema travels with the database and holds what no DDL expresses (pattern, enum, required,
|
|
4
|
+
// x-*). Reconstructing a schema from the database is lossy, so comparing the two for equality
|
|
5
|
+
// turns every gap into a change and rebuilds every table forever. Each property gets the question
|
|
6
|
+
// that fits it: capacity asks whether the column still holds what the schema wants (widen only),
|
|
7
|
+
// structural asks for equality, everything else is enforced by the application alone.
|
|
8
|
+
import { schemaType } from './sql.js'
|
|
9
|
+
|
|
10
|
+
const capacity = {
|
|
11
|
+
maximum: (need, have) => have >= need,
|
|
12
|
+
minimum: (need, have) => have <= need,
|
|
13
|
+
maxLength: (need, have) => have >= need,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const structural = ['format', 'contentEncoding', 'x-index', 'default']
|
|
17
|
+
|
|
18
|
+
/** @param dialectStructural properties this dialect renders too, e.g. 'multipleOf' on MySQL. */
|
|
19
|
+
export function fieldNeedsDdl(next, curr, dialectStructural = []) {
|
|
20
|
+
for (const [prop, holds] of Object.entries(capacity)) {
|
|
21
|
+
const need = next[prop], have = curr[prop]
|
|
22
|
+
// no requirement, or a bound the database never reported — nothing to widen towards
|
|
23
|
+
if (need !== undefined && have !== undefined && !holds(need, have)) return true
|
|
24
|
+
}
|
|
25
|
+
if (schemaType(next) !== schemaType(curr)) return true
|
|
26
|
+
if (!!next['x-autoincrement'] !== !!curr['x-autoincrement']) return true
|
|
27
|
+
return [...structural, ...dialectStructural].some(prop => next[prop] !== curr[prop])
|
|
28
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// sqlite/from-db.js
|
|
2
2
|
import { schemaFromDb as _schemaFromDb } from '../shared/from-db.js'
|
|
3
|
-
import { quoteId } from '../shared/sql.js'
|
|
3
|
+
import { queryRows, quoteId } from '../shared/sql.js'
|
|
4
4
|
import { schemaFromField } from './from-field.js'
|
|
5
5
|
|
|
6
6
|
/** Single-column, non-constraint indexes: [{ name, column, unique }]. */
|
|
7
7
|
export async function singleColumnIndexes(query, table) {
|
|
8
8
|
const out = []
|
|
9
|
-
for (const row of await query
|
|
9
|
+
for (const row of await queryRows(query, `PRAGMA index_list(${quoteId(table)})`)) {
|
|
10
10
|
if (row.origin && row.origin !== 'c') continue
|
|
11
|
-
const cols = await query
|
|
11
|
+
const cols = await queryRows(query, `PRAGMA index_info(${quoteId(row.name)})`)
|
|
12
12
|
if (cols.length === 1) out.push({ name: row.name, column: cols[0].name, unique: !!row.unique })
|
|
13
13
|
}
|
|
14
14
|
return out
|
|
@@ -22,11 +22,15 @@ async function indexes(query, table) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const dialect = {
|
|
25
|
-
tables: async (query) => (await query
|
|
25
|
+
tables: async (query) => (await queryRows(query, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")).map(r => r.name),
|
|
26
26
|
fields: async (query, table) => {
|
|
27
27
|
const ix = await indexes(query, table)
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// AUTOINCREMENT survives only in the stored DDL — no PRAGMA reports it
|
|
29
|
+
const [master] = await queryRows(query, `SELECT sql FROM sqlite_master WHERE type='table' AND name='${table.replaceAll("'", "''")}'`)
|
|
30
|
+
const auto = /\bautoincrement\b/i.test(master?.sql ?? '')
|
|
31
|
+
return (await queryRows(query, `PRAGMA table_info(${quoteId(table)})`)).map(row =>
|
|
32
|
+
row.pk ? { ...row, autoincrement: auto }
|
|
33
|
+
: ix.has(row.name) ? { ...row, index: ix.get(row.name) } : row
|
|
30
34
|
)
|
|
31
35
|
},
|
|
32
36
|
fromField: (row) => ({
|
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
// PRAGMA table_info returns: cid, name, type, notnull, dflt_value, pk
|
|
3
3
|
// SQLite lengths are in characters — no byte conversion needed
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
integer: { signed: [-2147483648, 2147483647] },
|
|
11
|
-
bigint: { signed: [-9007199254740991, 9007199254740991] }, // JS safe
|
|
12
|
-
}
|
|
5
|
+
// SQLite does not enforce a declared integer width — a TINYINT column happily stores 5000.
|
|
6
|
+
// So every spelling reports the same, real bound: the INTEGER storage class, capped where a
|
|
7
|
+
// JS number stops being exact.
|
|
8
|
+
const intTypes = new Set(['tinyint', 'smallint', 'mediumint', 'int', 'integer', 'bigint'])
|
|
9
|
+
const intMax = 9007199254740991
|
|
13
10
|
|
|
14
11
|
export function schemaFromField(row) {
|
|
15
12
|
const prop = {}
|
|
@@ -18,9 +15,8 @@ export function schemaFromField(row) {
|
|
|
18
15
|
const type = m?.[1]?.toLowerCase() ?? 'text'
|
|
19
16
|
const len = m?.[2] ? parseInt(m[2]) : null
|
|
20
17
|
|
|
21
|
-
if (type
|
|
22
|
-
|
|
23
|
-
prop.type = 'integer'; prop.minimum = min; prop.maximum = max
|
|
18
|
+
if (intTypes.has(type)) {
|
|
19
|
+
prop.type = 'integer'; prop.minimum = -intMax; prop.maximum = intMax
|
|
24
20
|
|
|
25
21
|
} else if (type === 'boolean' || type === 'bool') { prop.type = 'boolean'
|
|
26
22
|
} else if (type === 'real' || type === 'float' || type === 'double') { prop.type = 'number'
|
|
@@ -41,9 +37,20 @@ export function schemaFromField(row) {
|
|
|
41
37
|
prop.type = 'string'
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
if (row.dflt_value != null) prop.default = row.dflt_value
|
|
40
|
+
if (row.dflt_value != null) prop.default = parseDefault(row.dflt_value, prop.type)
|
|
45
41
|
if (row.pk) prop['x-index'] = 'primary'
|
|
46
42
|
else if (row.index) prop['x-index'] = row.index
|
|
43
|
+
if (row.autoincrement) prop['x-autoincrement'] = true
|
|
47
44
|
|
|
48
45
|
return prop
|
|
49
46
|
}
|
|
47
|
+
|
|
48
|
+
/** PRAGMA hands back the raw SQL literal — read it as the value the schema would declare. */
|
|
49
|
+
function parseDefault(raw, type) {
|
|
50
|
+
const v = String(raw).trim()
|
|
51
|
+
const n = Number(v)
|
|
52
|
+
if (/^'[\s\S]*'$/.test(v)) return v.slice(1, -1).replaceAll("''", "'")
|
|
53
|
+
if (type === 'boolean') return v !== '0'
|
|
54
|
+
if ((type === 'integer' || type === 'number') && !isNaN(n)) return n
|
|
55
|
+
return v
|
|
56
|
+
}
|
|
@@ -4,6 +4,7 @@ import { schemaFromDb, singleColumnIndexes } from './from-db.js'
|
|
|
4
4
|
import { toFieldDef } from './to-field.js'
|
|
5
5
|
import { quoteId, schemaType } from '../shared/sql.js'
|
|
6
6
|
import { schemaDiff } from '../../diff.js'
|
|
7
|
+
import { fieldNeedsDdl } from '../shared/needs-ddl.js'
|
|
7
8
|
|
|
8
9
|
function tableFields(tableSchema) {
|
|
9
10
|
return Object.entries(tableSchema.additionalProperties?.properties ?? {})
|
|
@@ -88,13 +89,17 @@ export async function schemaToDb(schema, query, { force = false, patch = false }
|
|
|
88
89
|
stmts.push(`CREATE TABLE ${quoteId(table)} (\n${createBody(fields, primaries)}\n);`)
|
|
89
90
|
stmts.push(...indexStatements(table, fields, primaries))
|
|
90
91
|
} else {
|
|
91
|
-
const currFields =
|
|
92
|
-
const
|
|
93
|
-
|
|
92
|
+
const currFields = new Map(tableFields(current.properties[table]))
|
|
93
|
+
const nextFieldNames = new Set(fields.map(([n]) => n))
|
|
94
|
+
// A new plain column is an ADD COLUMN; only a new key column needs the rebuild.
|
|
95
|
+
const hasChanges = fields.some(([name, prop]) => currFields.has(name)
|
|
96
|
+
? fieldNeedsDdl(prop, currFields.get(name))
|
|
97
|
+
: prop['x-index'] === 'primary' || prop['x-autoincrement'])
|
|
98
|
+
const hasDrops = !patch && [...currFields.keys()].some(n => !nextFieldNames.has(n))
|
|
94
99
|
|
|
95
|
-
if (hasChanges &&
|
|
100
|
+
if (hasDrops || hasChanges && !patch) {
|
|
96
101
|
const tmp = `${table}_migration_tmp`
|
|
97
|
-
const keep = fields.map(([n]) => n).filter(n => currFields.
|
|
102
|
+
const keep = fields.map(([n]) => n).filter(n => currFields.has(n))
|
|
98
103
|
const colsList = keep.map(quoteId).join(', ')
|
|
99
104
|
|
|
100
105
|
stmts.push(`CREATE TABLE ${quoteId(tmp)} (\n${createBody(fields, primaries)}\n);`)
|
|
@@ -104,7 +109,7 @@ export async function schemaToDb(schema, query, { force = false, patch = false }
|
|
|
104
109
|
stmts.push(...indexStatements(table, fields, primaries))
|
|
105
110
|
} else {
|
|
106
111
|
for (const [name, prop] of fields) {
|
|
107
|
-
if (!currFields.
|
|
112
|
+
if (!currFields.has(name))
|
|
108
113
|
stmts.push(`ALTER TABLE ${quoteId(table)} ADD COLUMN ${toFieldDef(name, prop)};`)
|
|
109
114
|
}
|
|
110
115
|
stmts.push(...indexStatements(table, fields, primaries, await singleColumnIndexes(query, table), { patch }))
|