masterrecord 0.1.3 → 0.2.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/Entity/entityTrackerModel.js +7 -3
- package/MIGRATIONS.md +178 -0
- package/Migrations/cli.js +3 -3
- package/Migrations/migrationMySQLQuery.js +18 -8
- package/Migrations/migrationSQLiteQuery.js +30 -3
- package/Migrations/schema.js +201 -16
- package/QueryLanguage/queryMethods.js +45 -58
- package/QueryLanguage/queryScript.js +102 -35
- package/SQLLiteEngine.js +158 -61
- package/Tools.js +74 -29
- package/context.js +195 -61
- package/deleteManager.js +3 -3
- package/insertManager.js +128 -25
- package/masterrecord_all_files.txt +4646 -0
- package/mySQLEngine.js +159 -44
- package/package.json +5 -5
- package/readme.md +3 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
// version : 0.0.
|
|
2
|
+
// version : 0.0.9
|
|
3
3
|
var tools = require('../Tools');
|
|
4
4
|
class EntityTrackerModel {
|
|
5
5
|
|
|
@@ -29,6 +29,10 @@ class EntityTrackerModel {
|
|
|
29
29
|
set: function(value) {
|
|
30
30
|
modelClass.__state = "modified";
|
|
31
31
|
modelClass.__dirtyFields.push(modelField);
|
|
32
|
+
// ensure this entity is tracked on any modification
|
|
33
|
+
if(modelClass.__context && typeof modelClass.__context.__track === 'function'){
|
|
34
|
+
modelClass.__context.__track(modelClass);
|
|
35
|
+
}
|
|
32
36
|
if(typeof currentEntity[modelField].set === "function"){
|
|
33
37
|
this["__proto__"]["_" + modelField] = currentEntity[modelField].set(value);
|
|
34
38
|
}else{
|
|
@@ -157,7 +161,7 @@ class EntityTrackerModel {
|
|
|
157
161
|
var entityFieldJoinName = currentEntity[entityField].foreignTable === undefined? entityField : currentEntity[entityField].foreignTable;
|
|
158
162
|
var thirdEntity = this.__context[tools.capitalize(entityFieldJoinName)];
|
|
159
163
|
var firstJoiningID = joiningEntity.__entity[this.__entity.__name].foreignTable;
|
|
160
|
-
var secondJoiningID = joiningEntity.__entity
|
|
164
|
+
var secondJoiningID = Object.values(joiningEntity.__entity).find(e => e.foreignTable === ent.__name);
|
|
161
165
|
if(firstJoiningID && secondJoiningID )
|
|
162
166
|
{
|
|
163
167
|
var modelValue = ent.include(`p => p.${entityFieldJoinName}.select(j => j.${joiningEntity.__entity[this.__entity.__name].foreignKey})`).include(`p =>p.${this.__entity.__name}`).where(`r =>r.${this.__entity.__name}.${priKey} = ${this[priKey]}`).toList();
|
|
@@ -234,4 +238,4 @@ class EntityTrackerModel {
|
|
|
234
238
|
|
|
235
239
|
}
|
|
236
240
|
|
|
237
|
-
module.exports = EntityTrackerModel
|
|
241
|
+
module.exports = EntityTrackerModel;
|
package/MIGRATIONS.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
### Migrations and Server Update Guide
|
|
2
|
+
|
|
3
|
+
This project ships a CLI, exposed as `masterrecord`, to manage database migrations. Below are the steps to enable migrations, create migrations, apply them, and update your running server.
|
|
4
|
+
|
|
5
|
+
### 1) Install the CLI (local repo checkout)
|
|
6
|
+
|
|
7
|
+
- From the project root, install the CLI globally:
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g ./
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
After install, the `masterrecord` command becomes available in your shell.
|
|
13
|
+
|
|
14
|
+
### 2) Prepare your Context and Environment
|
|
15
|
+
|
|
16
|
+
- Ensure your app has a Context class that extends `context` and configures a DB connection (SQLite or MySQL) using either `useSqlite()` or `useMySql()`.
|
|
17
|
+
- Set the environment via the `master` env var when running commands, e.g. `master=development` or `master=production`.
|
|
18
|
+
- Provide environment JSON at `env.<ENV>.json` reachable from your app root, keyed by your Context class name. Example for SQLite:
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"AppContext": {
|
|
22
|
+
"type": "better-sqlite3",
|
|
23
|
+
"connection": "/db/app.sqlite"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
Example for MySQL:
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"AppContext": {
|
|
31
|
+
"type": "mysql",
|
|
32
|
+
"host": "localhost",
|
|
33
|
+
"user": "root",
|
|
34
|
+
"password": "secret",
|
|
35
|
+
"database": "app_db"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3) Enable migrations (one-time per Context)
|
|
41
|
+
|
|
42
|
+
- Run from the project root where your Context file lives. Use the Context file name (without extension) as the argument.
|
|
43
|
+
```bash
|
|
44
|
+
master=development masterrecord enable-migrations AppContext
|
|
45
|
+
```
|
|
46
|
+
This creates `db/migrations/<context>_contextSnapShot.json` and the `db/migrations` directory.
|
|
47
|
+
|
|
48
|
+
### 4) Create a migration
|
|
49
|
+
|
|
50
|
+
- After you change your entity models, generate a migration file:
|
|
51
|
+
```bash
|
|
52
|
+
master=development masterrecord add-migration <MigrationName> AppContext
|
|
53
|
+
```
|
|
54
|
+
This writes a new file to `db/migrations/<timestamp>_<MigrationName>_migration.js`.
|
|
55
|
+
|
|
56
|
+
### 5) Apply migrations to the database
|
|
57
|
+
|
|
58
|
+
- Apply only the latest pending migration:
|
|
59
|
+
```bash
|
|
60
|
+
master=development masterrecord update-database AppContext
|
|
61
|
+
```
|
|
62
|
+
- Apply all migrations from the beginning (useful for a clean DB):
|
|
63
|
+
```bash
|
|
64
|
+
master=development masterrecord update-database-restart AppContext
|
|
65
|
+
```
|
|
66
|
+
- List migration files (debug/inspection):
|
|
67
|
+
```bash
|
|
68
|
+
master=development masterrecord get-migrations AppContext
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Notes:
|
|
72
|
+
- The CLI searches for `<context>_contextSnapShot.json` under `db/migrations` relative to your current working directory.
|
|
73
|
+
- For MySQL, ensure your credentials allow DDL. For SQLite, the data directory is created if missing.
|
|
74
|
+
|
|
75
|
+
### 6) Updating the running server
|
|
76
|
+
|
|
77
|
+
General flow to roll out schema changes:
|
|
78
|
+
- Stop the server or put it into maintenance mode (optional but recommended for non-backward-compatible changes).
|
|
79
|
+
- Pull the latest code (containing updated models and generated migration files).
|
|
80
|
+
- Run migrations against the target environment:
|
|
81
|
+
```bash
|
|
82
|
+
master=production masterrecord update-database AppContext
|
|
83
|
+
```
|
|
84
|
+
- Restart your server/process manager (e.g., `pm2 restart <app>`, `docker compose up -d`, or your platform’s restart command).
|
|
85
|
+
|
|
86
|
+
Backward-compatible rollout tip:
|
|
87
|
+
- If possible, deploy additive changes first (new tables/columns), release app code that begins using them, then later clean up/removal migrations.
|
|
88
|
+
|
|
89
|
+
### Troubleshooting
|
|
90
|
+
|
|
91
|
+
- Cannot find Context file: ensure you run commands from the app root and pass the correct Context file name used when defining your class (case-insensitive in the snapshot, but supply the same name you used).
|
|
92
|
+
- Cannot connect to DB: confirm `master=<env>` is set and `env.<env>.json` exists with correct credentials and paths.
|
|
93
|
+
- MySQL type mismatches: the migration engine maps MasterRecord types to SQL types; verify your entity field `type` values are correct.
|
|
94
|
+
|
|
95
|
+
### Recent improvements (2025-09)
|
|
96
|
+
|
|
97
|
+
- Query language and SQL engines:
|
|
98
|
+
- Correct parsing of multi-char operators (>=, <=, ===, !==) and spaced logical operators.
|
|
99
|
+
- Support for grouped OR conditions rendered as parenthesized OR in WHERE across SQLite/MySQL.
|
|
100
|
+
- Resilient fallback for partially parsed expressions.
|
|
101
|
+
- Relationships:
|
|
102
|
+
- `hasManyThrough` supported in insert and delete cascades.
|
|
103
|
+
- Environment file discovery:
|
|
104
|
+
- Context now walks up directories to find `config/environments/env.<env>.json`; fixed error throwing.
|
|
105
|
+
- Migrations (DDL generation):
|
|
106
|
+
- Default values emitted for SQLite/MySQL (including boolean coercion).
|
|
107
|
+
- `CREATE TABLE IF NOT EXISTS` to avoid failures when rerunning.
|
|
108
|
+
- Table introspection added; existing tables are synced: missing columns are added, MySQL applies `ALTER ... MODIFY` for NULL/DEFAULT changes, SQLite rebuilds table when necessary.
|
|
109
|
+
- Migration API additions in `schema.js`:
|
|
110
|
+
- `renameColumn(table)` implemented for SQLite/MySQL.
|
|
111
|
+
- `seed(tableName, rows)` implemented for bulk/single inserts with safe quoting.
|
|
112
|
+
|
|
113
|
+
### Using renameColumn and seed in migrations
|
|
114
|
+
|
|
115
|
+
Basic migration skeleton (generated by CLI):
|
|
116
|
+
```js
|
|
117
|
+
var masterrecord = require('masterrecord');
|
|
118
|
+
|
|
119
|
+
class AddSettings extends masterrecord.schema {
|
|
120
|
+
constructor(context){ super(context); }
|
|
121
|
+
|
|
122
|
+
up(table){
|
|
123
|
+
this.init(table);
|
|
124
|
+
// Add a new table
|
|
125
|
+
this.createTable(table.MailSettings);
|
|
126
|
+
|
|
127
|
+
// Rename a column on an existing table
|
|
128
|
+
this.renameColumn({ tableName: 'MailSettings', name: 'from_email', newName: 'reply_to' });
|
|
129
|
+
|
|
130
|
+
// Seed initial data (single row)
|
|
131
|
+
this.seed('MailSettings', {
|
|
132
|
+
from_name: 'System',
|
|
133
|
+
reply_to: 'no-reply@example.com',
|
|
134
|
+
return_path_matches_from: 0,
|
|
135
|
+
weekly_summary_enabled: 0,
|
|
136
|
+
created_at: Date.now(),
|
|
137
|
+
updated_at: Date.now()
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Seed multiple rows
|
|
141
|
+
this.seed('MailSettings', [
|
|
142
|
+
{ from_name: 'Support', reply_to: 'support@example.com', created_at: Date.now(), updated_at: Date.now() },
|
|
143
|
+
{ from_name: 'Marketing', reply_to: 'marketing@example.com', created_at: Date.now(), updated_at: Date.now() }
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
down(table){
|
|
148
|
+
this.init(table);
|
|
149
|
+
// Revert the rename
|
|
150
|
+
this.renameColumn({ tableName: 'MailSettings', name: 'reply_to', newName: 'from_email' });
|
|
151
|
+
|
|
152
|
+
// Optionally clean up seeded rows
|
|
153
|
+
// this.context._execute("DELETE FROM MailSettings WHERE reply_to IN ('no-reply@example.com','support@example.com','marketing@example.com')");
|
|
154
|
+
|
|
155
|
+
// Drop table if that was part of up
|
|
156
|
+
// this.dropTable(table.MailSettings);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
module.exports = AddSettings;
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Notes:
|
|
163
|
+
- `renameColumn` expects an object: `{ tableName, name, newName }` and works in both SQLite and MySQL.
|
|
164
|
+
- `seed(tableName, rows)` accepts:
|
|
165
|
+
- a single object: `{ col: value, ... }`
|
|
166
|
+
- or an array of objects: `[{...}, {...}]`
|
|
167
|
+
Values are auto-quoted; booleans become 1/0.
|
|
168
|
+
- When a table already exists, `update-database` will sync schema:
|
|
169
|
+
- Add missing columns.
|
|
170
|
+
- MySQL: adjust default/nullability via `ALTER ... MODIFY`.
|
|
171
|
+
- SQLite: rebuilds the table when nullability/default/type changes require it.
|
|
172
|
+
|
|
173
|
+
### Tips
|
|
174
|
+
- Prefer additive changes (add columns) before destructive changes (drops/renames) to minimize downtime.
|
|
175
|
+
- For large SQLite tables, a rebuild copies data; consider maintenance windows.
|
|
176
|
+
- Use `master=development masterrecord get-migrations AppContext` to inspect migration order.
|
|
177
|
+
|
|
178
|
+
|
package/Migrations/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// version 0.0.
|
|
3
|
+
// version 0.0.5
|
|
4
4
|
// https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/
|
|
5
5
|
// how to add environment variables on cli call example - master=development masterrecord add-migration auth authContext
|
|
6
6
|
|
|
7
|
-
const program = require('commander');
|
|
7
|
+
const { program } = require('commander');
|
|
8
8
|
let fs = require('fs');
|
|
9
9
|
let path = require('path');
|
|
10
10
|
var Migration = require('./migrations');
|
|
@@ -16,7 +16,7 @@ const [,, ...args] = process.argv
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
program
|
|
19
|
-
.version('0.0.3'
|
|
19
|
+
.version('0.0.3')
|
|
20
20
|
.description('A ORM framework that facilitates the creation and use of business objects whose data requires persistent storage to a database');
|
|
21
21
|
|
|
22
22
|
// Instructions : to run command you must go to main project folder is located and run the command using the context file name.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
// verison 0.0.
|
|
2
|
+
// verison 0.0.4
|
|
3
3
|
class migrationMySQLQuery {
|
|
4
4
|
|
|
5
5
|
#tempTableName = "_temp_alter_column_update"
|
|
@@ -37,14 +37,24 @@ class migrationMySQLQuery {
|
|
|
37
37
|
var unique = table.unique ? " UNIQUE" : "";
|
|
38
38
|
var type = this.typeManager(table.type);
|
|
39
39
|
var tableName = table.name;
|
|
40
|
-
|
|
41
|
-
if(table.default != null){
|
|
42
|
-
|
|
43
|
-
defaultValue = ` DEFAULT ${this.boolType(table.default)}`
|
|
44
|
-
}
|
|
45
|
-
if(table.relationshipType === 'belongsTo'){
|
|
40
|
+
if(table.relationshipType === 'belongsTo' && table.foreignKey){
|
|
46
41
|
tableName = table.foreignKey;
|
|
47
42
|
}
|
|
43
|
+
var defaultValue = "";
|
|
44
|
+
if(table.default !== undefined && table.default !== null){
|
|
45
|
+
let def = table.default;
|
|
46
|
+
if(table.type === 'boolean'){
|
|
47
|
+
def = this.boolType(def);
|
|
48
|
+
defaultValue = ` DEFAULT ${def}`;
|
|
49
|
+
}
|
|
50
|
+
else if(table.type === 'integer' || table.type === 'float' || table.type === 'decimal'){
|
|
51
|
+
defaultValue = ` DEFAULT ${def}`;
|
|
52
|
+
}
|
|
53
|
+
else{
|
|
54
|
+
const esc = String(def).replace(/'/g, "''");
|
|
55
|
+
defaultValue = ` DEFAULT '${esc}'`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
48
58
|
|
|
49
59
|
return `${tableName} ${type}${nullName}${defaultValue}${unique}${primaryKey}${auto}`;
|
|
50
60
|
}
|
|
@@ -176,7 +186,7 @@ class migrationMySQLQuery {
|
|
|
176
186
|
}
|
|
177
187
|
}
|
|
178
188
|
|
|
179
|
-
var completeQuery = `CREATE TABLE ${table.__name} (${queryVar.replace(/,\s*$/, "")});`;
|
|
189
|
+
var completeQuery = `CREATE TABLE IF NOT EXISTS ${table.__name} (${queryVar.replace(/,\s*$/, "")});`;
|
|
180
190
|
return completeQuery;
|
|
181
191
|
|
|
182
192
|
/*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
// verison 0.0.
|
|
2
|
+
// verison 0.0.7
|
|
3
3
|
class migrationSQLiteQuery {
|
|
4
4
|
|
|
5
5
|
#tempTableName = "_temp_alter_column_update"
|
|
@@ -36,8 +36,28 @@ class migrationSQLiteQuery {
|
|
|
36
36
|
var nullName = table.nullable ? "" : " NOT NULL";
|
|
37
37
|
var unique = table.unique ? " UNIQUE" : "";
|
|
38
38
|
var type = this.#typeManager(table.type);
|
|
39
|
+
var colName = table.name;
|
|
40
|
+
if(table.relationshipType === 'belongsTo' && table.foreignKey){
|
|
41
|
+
colName = table.foreignKey;
|
|
42
|
+
}
|
|
43
|
+
// DEFAULT clause
|
|
44
|
+
var defaultClause = "";
|
|
45
|
+
if(table.default !== undefined && table.default !== null){
|
|
46
|
+
let def = table.default;
|
|
47
|
+
if(table.type === 'boolean'){
|
|
48
|
+
def = (def === true || def === 'true') ? 1 : 0;
|
|
49
|
+
defaultClause = ` DEFAULT ${def}`;
|
|
50
|
+
}
|
|
51
|
+
else if(table.type === 'integer' || table.type === 'float' || table.type === 'decimal'){
|
|
52
|
+
defaultClause = ` DEFAULT ${def}`;
|
|
53
|
+
}
|
|
54
|
+
else{
|
|
55
|
+
const esc = String(def).replace(/'/g, "''");
|
|
56
|
+
defaultClause = ` DEFAULT '${esc}'`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
39
59
|
|
|
40
|
-
return `${
|
|
60
|
+
return `${colName} ${type}${nullName}${defaultClause}${unique}${primaryKey}${auto}`;
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
#typeManager(type){
|
|
@@ -75,6 +95,13 @@ class migrationSQLiteQuery {
|
|
|
75
95
|
|
|
76
96
|
|
|
77
97
|
addColum(table){
|
|
98
|
+
// If a full column spec is provided, map it to a proper SQLite column definition
|
|
99
|
+
if(table.column){
|
|
100
|
+
const def = this.#columnMapping(table.column);
|
|
101
|
+
return `ALTER TABLE ${table.tableName}
|
|
102
|
+
ADD COLUMN ${def}`;
|
|
103
|
+
}
|
|
104
|
+
// Fallback legacy behavior: raw name provided must include full definition if caller wants type/constraints
|
|
78
105
|
return `ALTER TABLE ${table.tableName}
|
|
79
106
|
ADD COLUMN ${table.name}`;
|
|
80
107
|
|
|
@@ -111,7 +138,7 @@ class migrationSQLiteQuery {
|
|
|
111
138
|
}
|
|
112
139
|
}
|
|
113
140
|
|
|
114
|
-
return `CREATE TABLE ${table.__name} (${queryVar.replace(/,\s*$/, "")});`;
|
|
141
|
+
return `CREATE TABLE IF NOT EXISTS ${table.__name} (${queryVar.replace(/,\s*$/, "")});`;
|
|
115
142
|
|
|
116
143
|
/*
|
|
117
144
|
INTEGER PRIMARY KEY AUTOINCREMENT
|
package/Migrations/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// version 0.0.
|
|
1
|
+
// version 0.0.5
|
|
2
2
|
class schema{
|
|
3
3
|
|
|
4
4
|
constructor(context){
|
|
@@ -65,24 +65,175 @@ class schema{
|
|
|
65
65
|
createTable(table){
|
|
66
66
|
|
|
67
67
|
if(table){
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
// If table exists, run sync instead of blind create
|
|
69
|
+
const tableName = table.__name;
|
|
70
|
+
if(this.context._SQLEngine.tableExists && this.context._SQLEngine.tableExists(tableName)){
|
|
71
|
+
this.syncTable(table);
|
|
72
|
+
} else {
|
|
73
|
+
if(this.context.isSQLite){
|
|
74
|
+
var sqliteQuery = require("./migrationSQLiteQuery");
|
|
75
|
+
var queryBuilder = new sqliteQuery();
|
|
76
|
+
var query = queryBuilder.createTable(table);
|
|
77
|
+
this.context._execute(query);
|
|
78
|
+
}
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
if(this.context.isMySQL){
|
|
81
|
+
var sqlquery = require("./migrationMySQLQuery");
|
|
82
|
+
var queryBuilder = new sqlquery();
|
|
83
|
+
var query = queryBuilder.createTable(table);
|
|
84
|
+
this.context._execute(query);
|
|
85
|
+
}
|
|
80
86
|
}
|
|
81
87
|
}else{
|
|
82
88
|
console.log("Table that your trying to create is undefined. PLease check if there are any changes that need to be made");
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
// Compute diffs and apply minimal changes
|
|
93
|
+
syncTable(table){
|
|
94
|
+
const engine = this.context._SQLEngine;
|
|
95
|
+
const tableName = table.__name;
|
|
96
|
+
const existing = engine.getTableInfo ? engine.getTableInfo(tableName) : [];
|
|
97
|
+
// Build a set of existing columns (sqlite: name, mysql: name)
|
|
98
|
+
const existingNames = new Set((existing || []).map(c => (c.name || c.COLUMN_NAME))); // both engines map to name
|
|
99
|
+
// Add missing columns only (safe path)
|
|
100
|
+
for (var key in table) {
|
|
101
|
+
if(typeof table[key] === 'object'){
|
|
102
|
+
const col = table[key];
|
|
103
|
+
// Skip relationships
|
|
104
|
+
if(col.type === 'hasOne' || col.type === 'hasMany' || col.type === 'hasManyThrough') continue;
|
|
105
|
+
const colName = (col.relationshipType === 'belongsTo' && col.foreignKey) ? col.foreignKey : col.name;
|
|
106
|
+
if(!existingNames.has(colName)){
|
|
107
|
+
// add column
|
|
108
|
+
const newCol = {
|
|
109
|
+
tableName: tableName,
|
|
110
|
+
name: colName,
|
|
111
|
+
type: col.type
|
|
112
|
+
};
|
|
113
|
+
// MySQL path uses addColum with realDataType
|
|
114
|
+
if(this.context.isSQLite){
|
|
115
|
+
var sqliteQuery = require("./migrationSQLiteQuery");
|
|
116
|
+
var queryBuilder = new sqliteQuery();
|
|
117
|
+
// Build a conservative column add (no NOT NULL without default)
|
|
118
|
+
const add = queryBuilder.addColum({ tableName, name: colName });
|
|
119
|
+
this.context._execute(add);
|
|
120
|
+
}
|
|
121
|
+
if(this.context.isMySQL){
|
|
122
|
+
var sqlquery = require("./migrationMySQLQuery");
|
|
123
|
+
var queryBuilder = new sqlquery();
|
|
124
|
+
newCol.realDataType = queryBuilder.typeManager(col.type);
|
|
125
|
+
const query = queryBuilder.addColum(newCol);
|
|
126
|
+
this.context._execute(query);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Detect modifications (nullable/default/type)
|
|
132
|
+
const desiredCols = [];
|
|
133
|
+
for (var key in table) {
|
|
134
|
+
if(typeof table[key] === 'object'){
|
|
135
|
+
const col = table[key];
|
|
136
|
+
if(col.type === 'hasOne' || col.type === 'hasMany' || col.type === 'hasManyThrough') continue;
|
|
137
|
+
const colName = (col.relationshipType === 'belongsTo' && col.foreignKey) ? col.foreignKey : col.name;
|
|
138
|
+
desiredCols.push({ name: colName, col });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const needRebuildSQLite = () => {
|
|
143
|
+
if(!this.context.isSQLite) return false;
|
|
144
|
+
const byName = {};
|
|
145
|
+
for(const row of existing){ byName[row.name] = row; }
|
|
146
|
+
for(const d of desiredCols){
|
|
147
|
+
const row = byName[d.name];
|
|
148
|
+
if(!row) continue;
|
|
149
|
+
const notnull = row.notnull === 1;
|
|
150
|
+
const desiredNotNull = d.col.nullable === false;
|
|
151
|
+
const desiredType = d.col.type;
|
|
152
|
+
const existingType = (row.type || '').toLowerCase();
|
|
153
|
+
// compare default (normalize quotes)
|
|
154
|
+
const exDefRaw = row.dflt_value == null ? null : String(row.dflt_value);
|
|
155
|
+
let exDef = exDefRaw;
|
|
156
|
+
if(typeof exDef === 'string' && exDef.length >= 2 && exDef.startsWith("'") && exDef.endsWith("'")){
|
|
157
|
+
exDef = exDef.slice(1, -1);
|
|
158
|
+
}
|
|
159
|
+
const dsDef = d.col.default == null ? null : String(d.col.default);
|
|
160
|
+
if(desiredNotNull !== notnull) return true;
|
|
161
|
+
if(exDef !== dsDef) return true;
|
|
162
|
+
// rough type differences that require rebuild
|
|
163
|
+
if((desiredType === 'boolean' && existingType !== 'integer') ||
|
|
164
|
+
(desiredType === 'string' && existingType !== 'text') ||
|
|
165
|
+
(desiredType === 'integer' && existingType !== 'integer')){
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if(this.context.isMySQL){
|
|
173
|
+
// Apply MODIFY for defaults/nullability
|
|
174
|
+
var sqlquery = require("./migrationMySQLQuery");
|
|
175
|
+
var queryBuilder = new sqlquery();
|
|
176
|
+
const byName = {};
|
|
177
|
+
for(const row of existing){ byName[row.name || row.COLUMN_NAME] = row; }
|
|
178
|
+
for(const d of desiredCols){
|
|
179
|
+
const row = byName[d.name];
|
|
180
|
+
if(!row) continue;
|
|
181
|
+
const desiredNotNull = d.col.nullable === false;
|
|
182
|
+
const existingNullable = (row.is_nullable || row.IS_NULLABLE || '').toString().toUpperCase() === 'YES';
|
|
183
|
+
// default normalize
|
|
184
|
+
const dsDef = d.col.default;
|
|
185
|
+
let exDef2 = row.dflt_value || row.COLUMN_DEFAULT;
|
|
186
|
+
if(typeof exDef2 === 'string' && exDef2.length >= 2 && exDef2.startsWith("'") && exDef2.endsWith("'")){
|
|
187
|
+
exDef2 = exDef2.slice(1, -1);
|
|
188
|
+
}
|
|
189
|
+
const differsNull = (desiredNotNull === true && existingNullable === true) || (desiredNotNull !== true && existingNullable === false);
|
|
190
|
+
const differsDef = (dsDef ?? null) !== (exDef2 ?? null);
|
|
191
|
+
if(differsNull || differsDef){
|
|
192
|
+
const type = queryBuilder.typeManager(d.col.type);
|
|
193
|
+
const nullPart = desiredNotNull ? 'NOT NULL' : 'NULL';
|
|
194
|
+
let defPart = '';
|
|
195
|
+
if(dsDef !== undefined && dsDef !== null){
|
|
196
|
+
if(d.col.type === 'boolean'){
|
|
197
|
+
defPart = ` DEFAULT ${queryBuilder.boolType(dsDef)}`;
|
|
198
|
+
} else if(d.col.type === 'integer' || d.col.type === 'float' || d.col.type === 'decimal'){
|
|
199
|
+
defPart = ` DEFAULT ${dsDef}`;
|
|
200
|
+
} else {
|
|
201
|
+
const esc = String(dsDef).replace(/'/g, "''");
|
|
202
|
+
defPart = ` DEFAULT '${esc}'`;
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
defPart = ' DEFAULT NULL';
|
|
206
|
+
}
|
|
207
|
+
const alter = `ALTER TABLE ${tableName} MODIFY COLUMN ${d.name} ${type} ${nullPart}${defPart}`;
|
|
208
|
+
this.context._execute(alter);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if(needRebuildSQLite()){
|
|
214
|
+
var sqliteQuery = require("./migrationSQLiteQuery");
|
|
215
|
+
var queryBuilder = new sqliteQuery();
|
|
216
|
+
// rename old table
|
|
217
|
+
const rename = queryBuilder.renameTable({ tableName, newName: "_temp_alter_column_update" });
|
|
218
|
+
this.context._execute(rename);
|
|
219
|
+
// create new with desired schema
|
|
220
|
+
const create = queryBuilder.createTable(table);
|
|
221
|
+
this.context._execute(create);
|
|
222
|
+
// compute common columns
|
|
223
|
+
const oldInfo = engine.getTableInfo(tableName.replace(/.*/, '_temp_alter_column_update')) || engine.getTableInfo("_temp_alter_column_update");
|
|
224
|
+
const oldNames = new Set((oldInfo || existing).map(r => r.name));
|
|
225
|
+
const newNames = desiredCols.map(d => d.name);
|
|
226
|
+
const common = newNames.filter(n => oldNames.has(n));
|
|
227
|
+
if(common.length > 0){
|
|
228
|
+
const cols = common.join(',');
|
|
229
|
+
const insert = `INSERT INTO ${tableName} (${cols}) SELECT ${cols} FROM _temp_alter_column_update`;
|
|
230
|
+
this.context._execute(insert);
|
|
231
|
+
}
|
|
232
|
+
const drop = queryBuilder.dropTable("_temp_alter_column_update");
|
|
233
|
+
this.context._execute(drop);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
86
237
|
|
|
87
238
|
dropTable(table){
|
|
88
239
|
if(table){
|
|
@@ -130,12 +281,46 @@ class schema{
|
|
|
130
281
|
}
|
|
131
282
|
}
|
|
132
283
|
|
|
133
|
-
renameColumn(){
|
|
134
|
-
|
|
284
|
+
renameColumn(table){
|
|
285
|
+
if(table){
|
|
286
|
+
if(this.context.isSQLite){
|
|
287
|
+
var sqliteQuery = require("./migrationSQLiteQuery");
|
|
288
|
+
var queryBuilder = new sqliteQuery();
|
|
289
|
+
var query = queryBuilder.renameColumn(table);
|
|
290
|
+
this.context._execute(query);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if(this.context.isMySQL){
|
|
294
|
+
var sqlquery = require("./migrationMySQLQuery");
|
|
295
|
+
var queryBuilder = new sqlquery();
|
|
296
|
+
var query = queryBuilder.renameColumn(table);
|
|
297
|
+
this.context._execute(query);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
135
300
|
}
|
|
136
301
|
|
|
137
|
-
seed(){
|
|
138
|
-
|
|
302
|
+
seed(tableName, rows){
|
|
303
|
+
if(!tableName || !rows){ return; }
|
|
304
|
+
const items = Array.isArray(rows) ? rows : [rows];
|
|
305
|
+
for(const row of items){
|
|
306
|
+
const cols = Object.keys(row);
|
|
307
|
+
if(cols.length === 0){ continue; }
|
|
308
|
+
const colList = cols.map(c => this.context.isSQLite ? `[${c}]` : `${c}`).join(", ");
|
|
309
|
+
const vals = cols.map(k => {
|
|
310
|
+
const v = row[k];
|
|
311
|
+
if(v === null || v === undefined){ return 'NULL'; }
|
|
312
|
+
if(typeof v === 'boolean'){
|
|
313
|
+
return this.context.isSQLite ? (v ? 1 : 0) : (v ? 1 : 0);
|
|
314
|
+
}
|
|
315
|
+
if(typeof v === 'number'){
|
|
316
|
+
return String(v);
|
|
317
|
+
}
|
|
318
|
+
const esc = String(v).replace(/'/g, "''");
|
|
319
|
+
return `'${esc}'`;
|
|
320
|
+
}).join(", ");
|
|
321
|
+
const sql = `INSERT INTO ${tableName} (${colList}) VALUES (${vals})`;
|
|
322
|
+
this.context._execute(sql);
|
|
323
|
+
}
|
|
139
324
|
}
|
|
140
325
|
|
|
141
326
|
}
|