From 3c575decbc4abf48e48da01b18bf909637d43ee1 Mon Sep 17 00:00:00 2001 From: Gioele Date: Fri, 26 Jan 2024 17:48:39 +0100 Subject: [PATCH 01/12] first query builder commit: added types, sqlitemanager with create table and tested everything --- jest.config.js | 4 +- package-lock.json | 4 +- src/index.ts | 12 ++ src/manager.ts | 169 +++++++++++++++++++++++++++++ src/types.ts | 144 ++++++++++++++++++++++++ test/assets/manager-test-tables.ts | 43 ++++++++ test/manager.test.ts | 33 ++++++ 7 files changed, 405 insertions(+), 4 deletions(-) create mode 100644 src/manager.ts create mode 100644 test/assets/manager-test-tables.ts create mode 100644 test/manager.test.ts diff --git a/jest.config.js b/jest.config.js index 54dbdde..b8cec38 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,9 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - testMatch: ['**/test/**/*.test.ts'], + testMatch: ['**/test/**/manager.test.ts'], maxWorkers: 12, - collectCoverageFrom: ['/src/**/*.ts', '!/src/index.ts', '!/src/types/**/*.ts'], + collectCoverageFrom: ['/src/**/types.ts', '/src/**/manager.ts'], transform: { '^.+\\.(ts|tsx)$': [ 'ts-jest', diff --git a/package-lock.json b/package-lock.json index eca9b35..79300a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sqlitecloud-js", - "version": "0.0.25", + "version": "0.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sqlitecloud-js", - "version": "0.0.25", + "version": "0.0.26", "license": "MIT", "dependencies": { "eventemitter3": "^5.0.1", diff --git a/src/index.ts b/src/index.ts index 72f02d9..6e54bb1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,3 +10,15 @@ export { SQLiteCloudRowset, SQLiteCloudRow } from './rowset' export { SQLiteCloudConnection } from './connection' export { escapeSqlParameter, prepareSql } from './utilities' + +export { SQLiteManager } from './manager' +export { + SQLiteManagerType, + SQLiteManagerColumn, + SQLiteManagerTable, + SQLiteManagerConstraints, + SQLiteManagerForeignKeyOn, + SQLiteManagerForeignKeyOptions, + SQLiteManagerCollate, + SQLiteManagerDefault +} from './types' diff --git a/src/manager.ts b/src/manager.ts new file mode 100644 index 0000000..7db756f --- /dev/null +++ b/src/manager.ts @@ -0,0 +1,169 @@ +/* eslint-disable prettier/prettier */ +import { + SQLiteManagerType, + SQLiteManagerColumn, + SQLiteManagerTable, + SQLiteManagerConstraints, + SQLiteManagerDefault, + SQLiteManagerCollate, + SQLiteManagerForeignKeyOptions, + SQLiteManagerForeignKeyOn +} from './types' + +export class SQLiteManager { + private table: SQLiteManagerTable + private create: boolean + + constructor(table?: SQLiteManagerTable) { + if (typeof table === 'undefined') { + this.create = true + this.table = {} as SQLiteManagerTable + } else { + if (table.name) { + this.create = true + } else { + this.create = false + } + this.table = table + } + } + + reset(): void { + this.table = {} as SQLiteManagerTable + } + + set name(name: string) { + this.table.name = name + } + + queryBuilder(): string { + let query = '' + + if (this.create && this.table.columns) { + query += 'CREATE TABLE "' + this.table.name + '" (' + + for (let j = 0; j < this.table.columns.length; j++) { + const column: SQLiteManagerColumn = this.table.columns[j] + + query += '"' + column.name + '" ' + SQLiteManagerType[column.type] + + if (column.constraints) { + const constraints: string[] = Object.keys(column.constraints).filter(key => { + if (column.constraints) { + return column.constraints[key as keyof SQLiteManagerConstraints] + } + }) + + constraints.forEach(constraint => { + query += ' ' + constraint.replace('_', ' ') + }) + + if (column.constraints.Check) { + query += ' CHECK (' + column.constraints.Check + ')' + } + + if (column.constraints.Default) { + query += ' DEFAULT ' + if (typeof column.constraints.Default === 'string') { + query += column.constraints.Default + } else { + query += SQLiteManagerDefault[column.constraints.Default] + } + } + + if (column.constraints.Collate) { + query += ' COLLATE ' + if (typeof column.constraints.Collate === 'string') { + query += column.constraints.Collate + } else { + query += SQLiteManagerCollate[column.constraints.Collate] + } + } + + if (column.constraints.ForeignKey) { + if (column.constraints.ForeignKey.enabled) { + query += ' REFERENCES ' + column.constraints.ForeignKey.table + '(' + column.constraints.ForeignKey.column + ')' + if (column.constraints.ForeignKey.options) { + query += ' ' + SQLiteManagerForeignKeyOptions[column.constraints.ForeignKey.options] + } + + if (column.constraints.ForeignKey.onDelete) { + query += ' ON DELETE ' + SQLiteManagerForeignKeyOn[column.constraints.ForeignKey.onDelete] + } + + if (column.constraints.ForeignKey.onUpdate) { + query += ' ON UPDATE ' + SQLiteManagerForeignKeyOn[column.constraints.ForeignKey.onUpdate] + } + } + } + } + + if (j < this.table.columns.length - 1) { + query += ', ' + } + } + + query += ');' + } + + return query + } + + mixTables(tables: SQLiteManagerTable, newTables: Partial): SQLiteManagerTable { + return { ...tables, ...newTables } + } + + addColumn(column: SQLiteManagerColumn): string { + this.table = this.mixTables(this.table, { name: this.table.name, columns: [Object.create(column)] }) + return this.queryBuilder() + } + + deleteColumn(columnName: string): string { + if (this.table.columns) { + const i = this.table.columns.findIndex(column => column.name === columnName) + if (i > -1) { + this.table.columns.splice(i, 1) + } + } + //this.tables[index].columns = this.tables[index].columns.filter(column => column.name !== columnName) it's slower + + return this.queryBuilder() + } + + renameColumn(oldColumnName: string, newColumnName: string): string { + if (this.table.columns) { + const i = this.table.columns.findIndex(column => column.name === oldColumnName) + if (i > -1) { + this.table.columns[i].name = newColumnName + } + } + + return this.queryBuilder() + } + + changeColumnType(columnName: string, type: SQLiteManagerType): string { + if (this.table.columns) { + const i = this.table.columns.findIndex(column => column.name === columnName) + if (i > -1) { + this.table.columns[i].type = type + } + } + + return this.queryBuilder() + } + + changeColumnConstraints(name: string, type: SQLiteManagerType, constraints: SQLiteManagerConstraints): string { + /* if (typeof this.table.columns != 'undefined') { + const i = this.table.columns.findIndex(column => column.name === columnName) + if (i > -1) { + this.table.columns[i].constraints = constraints + } + } */ + + this.table = this.mixTables(this.table, { + columns: [{ name: name, type: type, constraints: constraints }] + }) + + return this.queryBuilder() + } +} diff --git a/src/types.ts b/src/types.ts index 040d57a..63d7f9f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -129,3 +129,147 @@ export enum SQLiteCloudArrayType { ARRAY_TYPE_SQLITE_STATUS = 50 // used in sqlite_status } + +/** SQLite column types*/ +export enum SQLiteManagerType { + TEXT, + INTEGER, + REAL, + BLOB, + VARCHAR, + SMALLINT, + FLOAT, + DOUBLE, + BOOLEAN, + CURRENCY, + DATE, + TIME, + TIMESTAMP, + BINARY +} + +/** SQLite column defaults */ +export enum SQLiteManagerDefault { + NULL, + CURRENT_TIME, + CURRENT_DATE, + CURRENT_TIMESTAMP +} + +/** SQLite column collates */ +export enum SQLiteManagerCollate { + BINARY, + NOCASE, + RTRIM +} + +/** SQLite column foreign key options */ +export enum SQLiteManagerForeignKeyOptions { + NONE, + DEFERRABLE, + DEFERRABLE_INITIALLY_DEFERRED, + DEFERRABLE_INITIALLY_IMMEDIATE, + NOT_DEFERRABLE, + NOT_DEFERRABLE_INITIALLY_DEFERRED, + NOT_DEFERRABLE_INITIALLY_IMMEDIATE +} + +/** SQLite column foreign key on delete or on update cases */ +export enum SQLiteManagerForeignKeyOn { + NO_ACTION, + RESTRICT, + SET_NULL, + SET_DEFAULT, + CASCADE +} + +/** SQLite foreign key */ +class SQLiteManagerForeignKey { + enabled = false + table = '' + column = '' + options: SQLiteManagerForeignKeyOptions = SQLiteManagerForeignKeyOptions.NONE + onDelete: SQLiteManagerForeignKeyOn = SQLiteManagerForeignKeyOn.NO_ACTION + onUpdate: SQLiteManagerForeignKeyOn = SQLiteManagerForeignKeyOn.NO_ACTION + + constructor( + enabled: boolean, + table: string, + column: string, + options?: SQLiteManagerForeignKeyOptions, + onDelete?: SQLiteManagerForeignKeyOn, + onUpdate?: SQLiteManagerForeignKeyOn + ) { + if (enabled) { + this.enable(table, column, options, onDelete, onUpdate) + } else { + this.disable() + } + } + + /** By disabling foreign key you delete references */ + disable() { + this.enabled = false + this.table = '' + this.column = '' + this.options = SQLiteManagerForeignKeyOptions.NONE + this.onDelete = SQLiteManagerForeignKeyOn.NO_ACTION + this.onUpdate = SQLiteManagerForeignKeyOn.NO_ACTION + } + + enable(table: string, column: string, options?: SQLiteManagerForeignKeyOptions, onDelete?: SQLiteManagerForeignKeyOn, onUpdate?: SQLiteManagerForeignKeyOn) { + this.enabled = true + this.table = table + this.column = column + + if (options) { + this.options = options + } + + if (onDelete) { + this.onDelete = onDelete + } + + if (onUpdate) { + this.onUpdate = onUpdate + } + } +} + +/** SQLite column constraints */ +export interface SQLiteManagerConstraints { + PRIMARY_KEY?: boolean + AUTOINCREMENT?: boolean + NOT_NULL?: boolean + UNIQUE?: boolean + Check?: string + + /** You should pass: SQLiteManagerDefault.NULL, SQLiteManagerDefault.CURRENT_TIME, etc. */ + Default?: SQLiteManagerDefault | string + + /** You should pass: SQLiteManagerCollate.BINARY, SQLiteManagerCollate.NOCASE, etc. */ + Collate?: SQLiteManagerCollate | string + + ForeignKey?: SQLiteManagerForeignKey +} + +/** SQLite column interface */ +export interface SQLiteManagerColumn { + /** Column name */ + name: string + + /** Data type */ + type: SQLiteManagerType + + /** Constraints */ + constraints?: SQLiteManagerConstraints +} + +/** SQLite table interface */ +export interface SQLiteManagerTable { + /** Name of the table */ + name: string + + /** Columns */ + columns?: SQLiteManagerColumn[] +} diff --git a/test/assets/manager-test-tables.ts b/test/assets/manager-test-tables.ts new file mode 100644 index 0000000..2cdab9e --- /dev/null +++ b/test/assets/manager-test-tables.ts @@ -0,0 +1,43 @@ +/* eslint-disable prettier/prettier */ + +import * as types from '../../src/types' + +export const testTable = { + name: 'myTable', + columns: [ + { + name: 'column1', + type: types.SQLiteManagerType.INTEGER, + constraints: { + PRIMARY_KEY: true, + AUTOINCREMENT: true, + NOT_NULL: true, + UNIQUE: true + } + }, + { + name: 'column2', + type: types.SQLiteManagerType.TEXT, + constraints: { + NOT_NULL: true, + UNIQUE: true + } + }, + { + name: 'column3', + type: types.SQLiteManagerType.INTEGER, + constraints: { + NOT_NULL: true, + UNIQUE: true + } + }, + { + name: 'column4', + type: types.SQLiteManagerType.INTEGER, + constraints: { + NOT_NULL: true, + UNIQUE: true + } + } + ] +} diff --git a/test/manager.test.ts b/test/manager.test.ts new file mode 100644 index 0000000..643ad59 --- /dev/null +++ b/test/manager.test.ts @@ -0,0 +1,33 @@ +/* eslint-disable prettier/prettier */ +import { SQLiteManager } from '../src/manager' +import { SQLiteManagerType } from '../src/types' +import { testTable } from './assets/manager-test-tables' + +describe('Create a table', () => { + let manager: SQLiteManager + + it('tests create table', () => { + manager = new SQLiteManager({ name: testTable.name }) + + expect(manager.addColumn(testTable.columns[0])).toContain( + 'CREATE TABLE "' + testTable.name + '" ("' + testTable.columns[0].name + '" ' + SQLiteManagerType[testTable.columns[0].type] + ) + + manager.addColumn(testTable.columns[1]) + expect(manager.deleteColumn(testTable.columns[0].name)).not.toContain(testTable.columns[0].name) + + const risRen: string = manager.renameColumn(testTable.columns[1].name, testTable.columns[2].name) + + expect(risRen).not.toContain(testTable.columns[1].name) + expect(risRen).toContain(testTable.columns[2].name) + + const risCh: string = manager.changeColumnType(testTable.columns[2].name, SQLiteManagerType.TEXT) + + expect(risCh).toContain(SQLiteManagerType[SQLiteManagerType.TEXT]) + expect(risCh).not.toContain(SQLiteManagerType[SQLiteManagerType.INTEGER]) + + const risCnstr: string = manager.changeColumnConstraints(testTable.columns[2].name, testTable.columns[2].type, testTable.columns[2].constraints) + + expect(risCnstr).toContain('NOT NULL UNIQUE') + }) +}) From 59052ba626c7ebf525271ea3bf618db194dea824 Mon Sep 17 00:00:00 2001 From: Gioele Date: Mon, 29 Jan 2024 17:12:05 +0100 Subject: [PATCH 02/12] create table fixes, types fixes --- src/manager.ts | 67 +++++++++++++++--------------- src/types.ts | 43 ++++++++++--------- test/assets/manager-test-tables.ts | 2 +- test/manager.test.ts | 4 +- 4 files changed, 61 insertions(+), 55 deletions(-) diff --git a/src/manager.ts b/src/manager.ts index 7db756f..ddf2b5f 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -109,61 +109,62 @@ export class SQLiteManager { return query } - mixTables(tables: SQLiteManagerTable, newTables: Partial): SQLiteManagerTable { - return { ...tables, ...newTables } - } - addColumn(column: SQLiteManagerColumn): string { - this.table = this.mixTables(this.table, { name: this.table.name, columns: [Object.create(column)] }) + if (this.table.columns) { + this.table.columns.push(column) + } else { + this.table.columns = [column] + } + return this.queryBuilder() } - deleteColumn(columnName: string): string { - if (this.table.columns) { - const i = this.table.columns.findIndex(column => column.name === columnName) - if (i > -1) { - this.table.columns.splice(i, 1) - } + deleteColumn(name: string): string { + const i = this.findColumn(name) + + if (typeof i != 'undefined' && this.table.columns) { + this.table.columns.splice(i, 1) } - //this.tables[index].columns = this.tables[index].columns.filter(column => column.name !== columnName) it's slower return this.queryBuilder() } renameColumn(oldColumnName: string, newColumnName: string): string { - if (this.table.columns) { - const i = this.table.columns.findIndex(column => column.name === oldColumnName) - if (i > -1) { - this.table.columns[i].name = newColumnName - } + const i = this.findColumn(oldColumnName) + + if (typeof i != 'undefined' && this.table.columns) { + this.table.columns[i].name = newColumnName } return this.queryBuilder() } - changeColumnType(columnName: string, type: SQLiteManagerType): string { - if (this.table.columns) { - const i = this.table.columns.findIndex(column => column.name === columnName) - if (i > -1) { - this.table.columns[i].type = type - } + changeColumnType(name: string, type: SQLiteManagerType): string { + const i = this.findColumn(name) + + if (typeof i != 'undefined' && this.table.columns) { + this.table.columns[i].type = type } return this.queryBuilder() } - changeColumnConstraints(name: string, type: SQLiteManagerType, constraints: SQLiteManagerConstraints): string { - /* if (typeof this.table.columns != 'undefined') { - const i = this.table.columns.findIndex(column => column.name === columnName) - if (i > -1) { - this.table.columns[i].constraints = constraints - } - } */ + changeColumnConstraints(name: string, constraints: SQLiteManagerConstraints): string { + const i = this.findColumn(name) - this.table = this.mixTables(this.table, { - columns: [{ name: name, type: type, constraints: constraints }] - }) + if (typeof i != 'undefined' && this.table.columns) { + this.table.columns[i].constraints = constraints + } return this.queryBuilder() } + + private findColumn(name: string): number | undefined { + if (this.table.columns) { + const i = this.table.columns.findIndex(column => column.name === name) + if (i > -1) { + return i + } + } + } } diff --git a/src/types.ts b/src/types.ts index 63d7f9f..7e3a3f1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -130,25 +130,16 @@ export enum SQLiteCloudArrayType { ARRAY_TYPE_SQLITE_STATUS = 50 // used in sqlite_status } -/** SQLite column types*/ +/** SQLite Datatypes*/ export enum SQLiteManagerType { - TEXT, + NULL, INTEGER, REAL, - BLOB, - VARCHAR, - SMALLINT, - FLOAT, - DOUBLE, - BOOLEAN, - CURRENCY, - DATE, - TIME, - TIMESTAMP, - BINARY + TEXT, + BLOB } -/** SQLite column defaults */ +/** SQLite Default clause */ export enum SQLiteManagerDefault { NULL, CURRENT_TIME, @@ -156,7 +147,7 @@ export enum SQLiteManagerDefault { CURRENT_TIMESTAMP } -/** SQLite column collates */ +/** SQLite Collate clause */ export enum SQLiteManagerCollate { BINARY, NOCASE, @@ -174,7 +165,7 @@ export enum SQLiteManagerForeignKeyOptions { NOT_DEFERRABLE_INITIALLY_IMMEDIATE } -/** SQLite column foreign key on delete or on update cases */ +/** SQLite ON DELETE and ON UPDATE Actions */ export enum SQLiteManagerForeignKeyOn { NO_ACTION, RESTRICT, @@ -191,6 +182,7 @@ class SQLiteManagerForeignKey { options: SQLiteManagerForeignKeyOptions = SQLiteManagerForeignKeyOptions.NONE onDelete: SQLiteManagerForeignKeyOn = SQLiteManagerForeignKeyOn.NO_ACTION onUpdate: SQLiteManagerForeignKeyOn = SQLiteManagerForeignKeyOn.NO_ACTION + match = '' constructor( enabled: boolean, @@ -198,10 +190,11 @@ class SQLiteManagerForeignKey { column: string, options?: SQLiteManagerForeignKeyOptions, onDelete?: SQLiteManagerForeignKeyOn, - onUpdate?: SQLiteManagerForeignKeyOn + onUpdate?: SQLiteManagerForeignKeyOn, + match?: string ) { if (enabled) { - this.enable(table, column, options, onDelete, onUpdate) + this.enable(table, column, options, onDelete, onUpdate, match) } else { this.disable() } @@ -215,9 +208,17 @@ class SQLiteManagerForeignKey { this.options = SQLiteManagerForeignKeyOptions.NONE this.onDelete = SQLiteManagerForeignKeyOn.NO_ACTION this.onUpdate = SQLiteManagerForeignKeyOn.NO_ACTION + this.match = '' } - enable(table: string, column: string, options?: SQLiteManagerForeignKeyOptions, onDelete?: SQLiteManagerForeignKeyOn, onUpdate?: SQLiteManagerForeignKeyOn) { + enable( + table: string, + column: string, + options?: SQLiteManagerForeignKeyOptions, + onDelete?: SQLiteManagerForeignKeyOn, + onUpdate?: SQLiteManagerForeignKeyOn, + match?: string + ) { this.enabled = true this.table = table this.column = column @@ -233,6 +234,10 @@ class SQLiteManagerForeignKey { if (onUpdate) { this.onUpdate = onUpdate } + + if (match) { + this.match = match + } } } diff --git a/test/assets/manager-test-tables.ts b/test/assets/manager-test-tables.ts index 2cdab9e..27f546c 100644 --- a/test/assets/manager-test-tables.ts +++ b/test/assets/manager-test-tables.ts @@ -17,7 +17,7 @@ export const testTable = { }, { name: 'column2', - type: types.SQLiteManagerType.TEXT, + type: types.SQLiteManagerType.REAL, constraints: { NOT_NULL: true, UNIQUE: true diff --git a/test/manager.test.ts b/test/manager.test.ts index 643ad59..98c9f0f 100644 --- a/test/manager.test.ts +++ b/test/manager.test.ts @@ -13,7 +13,7 @@ describe('Create a table', () => { 'CREATE TABLE "' + testTable.name + '" ("' + testTable.columns[0].name + '" ' + SQLiteManagerType[testTable.columns[0].type] ) - manager.addColumn(testTable.columns[1]) + manager.addColumn(JSON.parse(JSON.stringify(testTable.columns[1]))) expect(manager.deleteColumn(testTable.columns[0].name)).not.toContain(testTable.columns[0].name) const risRen: string = manager.renameColumn(testTable.columns[1].name, testTable.columns[2].name) @@ -26,7 +26,7 @@ describe('Create a table', () => { expect(risCh).toContain(SQLiteManagerType[SQLiteManagerType.TEXT]) expect(risCh).not.toContain(SQLiteManagerType[SQLiteManagerType.INTEGER]) - const risCnstr: string = manager.changeColumnConstraints(testTable.columns[2].name, testTable.columns[2].type, testTable.columns[2].constraints) + const risCnstr: string = manager.changeColumnConstraints(testTable.columns[2].name, testTable.columns[2].constraints) expect(risCnstr).toContain('NOT NULL UNIQUE') }) From 8edf387b0df1a0d904891abdb35997112fb3ced2 Mon Sep 17 00:00:00 2001 From: Gioele Date: Mon, 29 Jan 2024 17:18:52 +0100 Subject: [PATCH 03/12] missing match in query builder --- src/manager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/manager.ts b/src/manager.ts index ddf2b5f..0c56116 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -94,6 +94,10 @@ export class SQLiteManager { if (column.constraints.ForeignKey.onUpdate) { query += ' ON UPDATE ' + SQLiteManagerForeignKeyOn[column.constraints.ForeignKey.onUpdate] } + + if (column.constraints.ForeignKey.match) { + query += ' MATCH ' + column.constraints.ForeignKey.match + } } } } From 0b9fc528cabbeb5d2e71827d6e96ae083756e149 Mon Sep 17 00:00:00 2001 From: Gioele Date: Mon, 29 Jan 2024 19:07:21 +0100 Subject: [PATCH 04/12] added sqlite directly supported alter table and tested it --- src/manager.ts | 165 +++++++++++++++++------------ src/types.ts | 8 ++ test/assets/manager-test-tables.ts | 41 +++++++ test/manager.test.ts | 36 ++++++- 4 files changed, 183 insertions(+), 67 deletions(-) diff --git a/src/manager.ts b/src/manager.ts index 0c56116..c04f6cf 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -7,12 +7,14 @@ import { SQLiteManagerDefault, SQLiteManagerCollate, SQLiteManagerForeignKeyOptions, - SQLiteManagerForeignKeyOn + SQLiteManagerForeignKeyOn, + AT } from './types' export class SQLiteManager { private table: SQLiteManagerTable - private create: boolean + private create = false + private query = '' constructor(table?: SQLiteManagerTable) { if (typeof table === 'undefined') { @@ -21,8 +23,9 @@ export class SQLiteManager { } else { if (table.name) { this.create = true - } else { - this.create = false + if (table.columns) { + this.create = false + } } this.table = table } @@ -32,84 +35,114 @@ export class SQLiteManager { this.table = {} as SQLiteManagerTable } + /** If changing name in altertable you need to manually call the queryBuilder() */ set name(name: string) { - this.table.name = name + if (this.create) { + this.table.name = name + } else { + this.table.name = name + this.queryBuilder(AT.RENAME_TABLE, { name: name } as SQLiteManagerColumn) + } } - queryBuilder(): string { + queryBuilder(op?: AT, column?: SQLiteManagerColumn, newColumn?: string): string { let query = '' - if (this.create && this.table.columns) { - query += 'CREATE TABLE "' + this.table.name + '" (' + if (this.table.columns) { + if (this.create) { + query += 'CREATE TABLE "' + this.table.name + '" (' + + for (let j = 0; j < this.table.columns.length; j++) { + query += this.queryBuilderColumn(this.table.columns[j]) + if (j < this.table.columns.length - 1) { + query += ', ' + } + } + + query += ');' + } else { + if (typeof op != 'undefined' && column) { + switch (op) { + case AT.RENAME_TABLE: + this.query += 'ALTER TABLE "' + this.table.name + '" RENAME TO "' + column.name + '";\n' + break + case AT.ADD_COLUMN: + this.query += 'ALTER TABLE "' + this.table.name + '" ADD COLUMN ' + this.queryBuilderColumn(column) + ';\n' + break + case AT.DROP_COLUMN: + this.query += 'ALTER TABLE "' + this.table.name + '" DROP COLUMN "' + column.name + '";\n' + break + case AT.RENAME_COLUMN: + if (newColumn) this.query += 'ALTER TABLE "' + this.table.name + '" RENAME COLUMN "' + column.name + '" TO "' + newColumn + '";\n' + break + } + } - for (let j = 0; j < this.table.columns.length; j++) { - const column: SQLiteManagerColumn = this.table.columns[j] + query = this.query + } + } - query += '"' + column.name + '" ' + SQLiteManagerType[column.type] + return query + } + + private queryBuilderColumn(column: SQLiteManagerColumn): string { + let query = '' + query += '"' + column.name + '" ' + SQLiteManagerType[column.type] + if (column.constraints) { + const constraints: string[] = Object.keys(column.constraints).filter(key => { if (column.constraints) { - const constraints: string[] = Object.keys(column.constraints).filter(key => { - if (column.constraints) { - return column.constraints[key as keyof SQLiteManagerConstraints] - } - }) - - constraints.forEach(constraint => { - query += ' ' + constraint.replace('_', ' ') - }) - - if (column.constraints.Check) { - query += ' CHECK (' + column.constraints.Check + ')' - } + return column.constraints[key as keyof SQLiteManagerConstraints] + } + }) + + constraints.forEach(constraint => { + query += ' ' + constraint.replace('_', ' ') + }) + + if (column.constraints.Check) { + query += ' CHECK (' + column.constraints.Check + ')' + } + + if (column.constraints.Default) { + query += ' DEFAULT ' + if (typeof column.constraints.Default === 'string') { + query += column.constraints.Default + } else { + query += SQLiteManagerDefault[column.constraints.Default] + } + } + + if (column.constraints.Collate) { + query += ' COLLATE ' + if (typeof column.constraints.Collate === 'string') { + query += column.constraints.Collate + } else { + query += SQLiteManagerCollate[column.constraints.Collate] + } + } - if (column.constraints.Default) { - query += ' DEFAULT ' - if (typeof column.constraints.Default === 'string') { - query += column.constraints.Default - } else { - query += SQLiteManagerDefault[column.constraints.Default] - } + if (column.constraints.ForeignKey) { + if (column.constraints.ForeignKey.enabled) { + query += ' REFERENCES ' + column.constraints.ForeignKey.table + '(' + column.constraints.ForeignKey.column + ')' + if (column.constraints.ForeignKey.options) { + query += ' ' + SQLiteManagerForeignKeyOptions[column.constraints.ForeignKey.options] } - if (column.constraints.Collate) { - query += ' COLLATE ' - if (typeof column.constraints.Collate === 'string') { - query += column.constraints.Collate - } else { - query += SQLiteManagerCollate[column.constraints.Collate] - } + if (column.constraints.ForeignKey.onDelete) { + query += ' ON DELETE ' + SQLiteManagerForeignKeyOn[column.constraints.ForeignKey.onDelete] } - if (column.constraints.ForeignKey) { - if (column.constraints.ForeignKey.enabled) { - query += ' REFERENCES ' + column.constraints.ForeignKey.table + '(' + column.constraints.ForeignKey.column + ')' - if (column.constraints.ForeignKey.options) { - query += ' ' + SQLiteManagerForeignKeyOptions[column.constraints.ForeignKey.options] - } - - if (column.constraints.ForeignKey.onDelete) { - query += ' ON DELETE ' + SQLiteManagerForeignKeyOn[column.constraints.ForeignKey.onDelete] - } - - if (column.constraints.ForeignKey.onUpdate) { - query += ' ON UPDATE ' + SQLiteManagerForeignKeyOn[column.constraints.ForeignKey.onUpdate] - } - - if (column.constraints.ForeignKey.match) { - query += ' MATCH ' + column.constraints.ForeignKey.match - } - } + if (column.constraints.ForeignKey.onUpdate) { + query += ' ON UPDATE ' + SQLiteManagerForeignKeyOn[column.constraints.ForeignKey.onUpdate] } - } - if (j < this.table.columns.length - 1) { - query += ', ' + if (column.constraints.ForeignKey.match) { + query += ' MATCH ' + column.constraints.ForeignKey.match + } } } - - query += ');' } - return query } @@ -120,7 +153,7 @@ export class SQLiteManager { this.table.columns = [column] } - return this.queryBuilder() + return this.queryBuilder(AT.ADD_COLUMN, column) } deleteColumn(name: string): string { @@ -130,7 +163,7 @@ export class SQLiteManager { this.table.columns.splice(i, 1) } - return this.queryBuilder() + return this.queryBuilder(AT.DROP_COLUMN, { name: name } as SQLiteManagerColumn) } renameColumn(oldColumnName: string, newColumnName: string): string { @@ -140,7 +173,7 @@ export class SQLiteManager { this.table.columns[i].name = newColumnName } - return this.queryBuilder() + return this.queryBuilder(AT.RENAME_COLUMN, { name: oldColumnName } as SQLiteManagerColumn, newColumnName) } changeColumnType(name: string, type: SQLiteManagerType): string { diff --git a/src/types.ts b/src/types.ts index 7e3a3f1..a975ebb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -278,3 +278,11 @@ export interface SQLiteManagerTable { /** Columns */ columns?: SQLiteManagerColumn[] } + +/** SQLite Alter Table */ +export enum AT { + ADD_COLUMN, + DROP_COLUMN, + RENAME_COLUMN, + RENAME_TABLE +} diff --git a/test/assets/manager-test-tables.ts b/test/assets/manager-test-tables.ts index 27f546c..15dfc8b 100644 --- a/test/assets/manager-test-tables.ts +++ b/test/assets/manager-test-tables.ts @@ -41,3 +41,44 @@ export const testTable = { } ] } + +export const testTable2 = { + name: 'myTable', + columns: [ + { + name: 'test1', + type: types.SQLiteManagerType.REAL, + constraints: { + PRIMARY_KEY: true, + AUTOINCREMENT: true, + NOT_NULL: true, + UNIQUE: true + } + }, + { + name: 'test2', + type: types.SQLiteManagerType.INTEGER, + constraints: { + NOT_NULL: true, + UNIQUE: true + } + }, + { + name: 'test3', + type: types.SQLiteManagerType.TEXT, + constraints: { + NOT_NULL: false, + UNIQUE: true + } + }, + { + name: 'test4', + type: types.SQLiteManagerType.BLOB, + constraints: { + NOT_NULL: true, + UNIQUE: true, + CHECK: 'test4 > 0' + } + } + ] +} diff --git a/test/manager.test.ts b/test/manager.test.ts index 98c9f0f..7626afe 100644 --- a/test/manager.test.ts +++ b/test/manager.test.ts @@ -1,7 +1,7 @@ /* eslint-disable prettier/prettier */ import { SQLiteManager } from '../src/manager' import { SQLiteManagerType } from '../src/types' -import { testTable } from './assets/manager-test-tables' +import { testTable, testTable2 } from './assets/manager-test-tables' describe('Create a table', () => { let manager: SQLiteManager @@ -30,4 +30,38 @@ describe('Create a table', () => { expect(risCnstr).toContain('NOT NULL UNIQUE') }) + + it('tests alter table', () => { + manager = new SQLiteManager(testTable) + + const addColumn: string = manager.addColumn(testTable2.columns[0]) + + expect(addColumn).toContain('ALTER TABLE') + expect(addColumn).toContain(testTable.name) + expect(addColumn).toContain('ADD COLUMN') + expect(addColumn).toContain(testTable2.columns[0].name) + expect(addColumn).toContain(SQLiteManagerType[testTable2.columns[0].type]) + + manager.addColumn(JSON.parse(JSON.stringify(testTable2.columns[1]))) + expect(manager.deleteColumn(testTable2.columns[0].name)).toContain('ALTER TABLE "' + testTable.name + '" DROP COLUMN "' + testTable2.columns[0].name + '";') + + const risRen: string = manager.renameColumn(testTable.columns[1].name, testTable2.columns[2].name) + + expect(risRen).toContain(testTable.columns[1].name) + expect(risRen).toContain(testTable2.columns[2].name) + + manager.name = testTable2.name + const renTable: string = manager.queryBuilder() + + expect(renTable).toContain('ALTER TABLE "' + testTable.name + '" RENAME TO "' + testTable2.name + '";') + + /* const risCh: string = manager.changeColumnType(testTable.columns[2].name, SQLiteManagerType.TEXT) + + expect(risCh).toContain(SQLiteManagerType[SQLiteManagerType.TEXT]) + expect(risCh).not.toContain(SQLiteManagerType[SQLiteManagerType.INTEGER]) + + const risCnstr: string = manager.changeColumnConstraints(testTable.columns[2].name, testTable.columns[2].constraints) + + expect(risCnstr).toContain('NOT NULL UNIQUE') */ + }) }) From 4e7940a1aaa8ad1f53a4214ef1e717a366c1d4fa Mon Sep 17 00:00:00 2001 From: Gioele Date: Tue, 30 Jan 2024 12:44:07 +0100 Subject: [PATCH 05/12] other kinds of table schema changes + related tests --- src/manager.ts | 20 +++++++++++++++++--- test/manager.test.ts | 19 ++++++++++++------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/manager.ts b/src/manager.ts index c04f6cf..f5e62f6 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -45,7 +45,7 @@ export class SQLiteManager { } } - queryBuilder(op?: AT, column?: SQLiteManagerColumn, newColumn?: string): string { + queryBuilder(op?: AT | string, column?: SQLiteManagerColumn, newColumn?: string): string { let query = '' if (this.table.columns) { @@ -62,6 +62,7 @@ export class SQLiteManager { query += ');' } else { if (typeof op != 'undefined' && column) { + let rand: string switch (op) { case AT.RENAME_TABLE: this.query += 'ALTER TABLE "' + this.table.name + '" RENAME TO "' + column.name + '";\n' @@ -74,6 +75,19 @@ export class SQLiteManager { break case AT.RENAME_COLUMN: if (newColumn) this.query += 'ALTER TABLE "' + this.table.name + '" RENAME COLUMN "' + column.name + '" TO "' + newColumn + '";\n' + break + default: + rand = Math.floor(Math.random() * 1000000000000).toString() + this.query += '\n\nPRAGMA foreign_keys = OFF;\n' + this.query += 'BEGIN TRANSACTION;\n' + this.query += 'ALTER TABLE "' + this.table.name + '" RENAME TO sqlitemanager_temp_table_' + rand + ';\n' + this.create = true + this.query += this.queryBuilder() + this.create = false + this.query += '\nDROP TABLE sqlitemanager_temp_table_' + rand + ';\n' + this.query += 'COMMIT;\n' + this.query += 'PRAGMA foreign_keys = ON;\n' + break } } @@ -183,7 +197,7 @@ export class SQLiteManager { this.table.columns[i].type = type } - return this.queryBuilder() + return this.queryBuilder('', {} as SQLiteManagerColumn) } changeColumnConstraints(name: string, constraints: SQLiteManagerConstraints): string { @@ -193,7 +207,7 @@ export class SQLiteManager { this.table.columns[i].constraints = constraints } - return this.queryBuilder() + return this.queryBuilder('', {} as SQLiteManagerColumn) } private findColumn(name: string): number | undefined { diff --git a/test/manager.test.ts b/test/manager.test.ts index 7626afe..14f01dd 100644 --- a/test/manager.test.ts +++ b/test/manager.test.ts @@ -55,13 +55,18 @@ describe('Create a table', () => { expect(renTable).toContain('ALTER TABLE "' + testTable.name + '" RENAME TO "' + testTable2.name + '";') - /* const risCh: string = manager.changeColumnType(testTable.columns[2].name, SQLiteManagerType.TEXT) - - expect(risCh).toContain(SQLiteManagerType[SQLiteManagerType.TEXT]) - expect(risCh).not.toContain(SQLiteManagerType[SQLiteManagerType.INTEGER]) - - const risCnstr: string = manager.changeColumnConstraints(testTable.columns[2].name, testTable.columns[2].constraints) + const risCh: string = manager.changeColumnType(testTable.columns[2].name, SQLiteManagerType.TEXT) - expect(risCnstr).toContain('NOT NULL UNIQUE') */ + expect(risCh).toContain('PRAGMA foreign_keys = OFF;') + expect(risCh).toContain('BEGIN TRANSACTION;') + expect(risCh).toContain('ALTER TABLE "' + testTable.name + '" RENAME TO sqlitemanager_temp_table_') + expect(risCh).toContain('CREATE TABLE "' + testTable.name + '" ("') + expect(risCh).toContain('"' + testTable.columns[2].name + '" TEXT') + expect(risCh).toContain('DROP TABLE sqlitemanager_temp_table_') + expect(risCh).toContain('COMMIT;') + expect(risCh).toContain('PRAGMA foreign_keys = ON;') + + const risCnstr: string = manager.changeColumnConstraints(testTable.columns[2].name, testTable2.columns[2].constraints) + expect(risCnstr).toContain('"' + testTable.columns[2].name + '" TEXT UNIQUE') }) }) From db6ab437c8bfe162e7ece07b9760f29d35a5d862 Mon Sep 17 00:00:00 2001 From: Gioele Date: Tue, 30 Jan 2024 14:58:31 +0100 Subject: [PATCH 06/12] updated the alter table with the correct 12-step generalized alter table procedure (TBF) --- src/manager.ts | 21 +++++++++++++++++---- test/manager.test.ts | 6 +++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/manager.ts b/src/manager.ts index f5e62f6..a51baf9 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -62,7 +62,7 @@ export class SQLiteManager { query += ');' } else { if (typeof op != 'undefined' && column) { - let rand: string + let oldname: string switch (op) { case AT.RENAME_TABLE: this.query += 'ALTER TABLE "' + this.table.name + '" RENAME TO "' + column.name + '";\n' @@ -77,14 +77,27 @@ export class SQLiteManager { if (newColumn) this.query += 'ALTER TABLE "' + this.table.name + '" RENAME COLUMN "' + column.name + '" TO "' + newColumn + '";\n' break default: - rand = Math.floor(Math.random() * 1000000000000).toString() this.query += '\n\nPRAGMA foreign_keys = OFF;\n' this.query += 'BEGIN TRANSACTION;\n' - this.query += 'ALTER TABLE "' + this.table.name + '" RENAME TO sqlitemanager_temp_table_' + rand + ';\n' + this.query += 'SELECT type, sql FROM sqlite_schema WHERE tbl_name=' + this.table.name + ';\n' this.create = true + oldname = this.table.name + this.table.name = 'new_' + this.table.name this.query += this.queryBuilder() this.create = false - this.query += '\nDROP TABLE sqlitemanager_temp_table_' + rand + ';\n' + this.query += '\nINSERT INTO "' + this.table.name + '" SELECT * FROM "' + oldname + '";\n' + this.query += 'DROP TABLE "' + oldname + '";\n' + this.query += 'ALTER TABLE "' + this.table.name + '" RENAME TO "' + oldname + '";\n' + this.table.name = oldname + /* +TODO +Use CREATE INDEX, CREATE TRIGGER, and CREATE VIEW to reconstruct indexes, triggers, and views associated with table X. Perhaps use the old format of the triggers, indexes, and views saved from step 3 above as a guide, making changes as appropriate for the alteration. + +If any views refer to table X in a way that is affected by the schema change, then drop those views using DROP VIEW and recreate them with whatever changes are necessary to accommodate the schema change using CREATE VIEW. + +If foreign key constraints were originally enabled then run PRAGMA foreign_key_check to verify that the schema change did not break any foreign key constraints. + +*/ this.query += 'COMMIT;\n' this.query += 'PRAGMA foreign_keys = ON;\n' diff --git a/test/manager.test.ts b/test/manager.test.ts index 14f01dd..c0fbf95 100644 --- a/test/manager.test.ts +++ b/test/manager.test.ts @@ -59,10 +59,10 @@ describe('Create a table', () => { expect(risCh).toContain('PRAGMA foreign_keys = OFF;') expect(risCh).toContain('BEGIN TRANSACTION;') - expect(risCh).toContain('ALTER TABLE "' + testTable.name + '" RENAME TO sqlitemanager_temp_table_') - expect(risCh).toContain('CREATE TABLE "' + testTable.name + '" ("') + expect(risCh).toContain('ALTER TABLE "new_' + testTable.name + '" RENAME TO "' + testTable.name + '";') + expect(risCh).toContain('CREATE TABLE "new_' + testTable.name + '" ("') expect(risCh).toContain('"' + testTable.columns[2].name + '" TEXT') - expect(risCh).toContain('DROP TABLE sqlitemanager_temp_table_') + expect(risCh).toContain('DROP TABLE "' + testTable.name) expect(risCh).toContain('COMMIT;') expect(risCh).toContain('PRAGMA foreign_keys = ON;') From bab2bb294a34d03a26edd381537fd11a7de113e4 Mon Sep 17 00:00:00 2001 From: Gioele Date: Tue, 30 Jan 2024 17:26:16 +0100 Subject: [PATCH 07/12] added duplicate column name check and foreign key check before committing other kinds of table schema changes --- src/manager.ts | 21 ++++++++++++++++----- test/assets/manager-test-tables.ts | 4 ++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/manager.ts b/src/manager.ts index a51baf9..445b014 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -82,7 +82,11 @@ export class SQLiteManager { this.query += 'SELECT type, sql FROM sqlite_schema WHERE tbl_name=' + this.table.name + ';\n' this.create = true oldname = this.table.name - this.table.name = 'new_' + this.table.name + if (typeof this.findColumn('new_' + this.table.name) == 'undefined') { + this.table.name = 'new_' + this.table.name + } else { + throw new Error('Column new_' + this.table.name + 'already exists') + } this.query += this.queryBuilder() this.create = false this.query += '\nINSERT INTO "' + this.table.name + '" SELECT * FROM "' + oldname + '";\n' @@ -95,9 +99,8 @@ Use CREATE INDEX, CREATE TRIGGER, and CREATE VIEW to reconstruct indexes, trigge If any views refer to table X in a way that is affected by the schema change, then drop those views using DROP VIEW and recreate them with whatever changes are necessary to accommodate the schema change using CREATE VIEW. -If foreign key constraints were originally enabled then run PRAGMA foreign_key_check to verify that the schema change did not break any foreign key constraints. - */ + this.query += 'PRAGMA foreign_key_check("' + this.table.name + '");\n' this.query += 'COMMIT;\n' this.query += 'PRAGMA foreign_keys = ON;\n' @@ -175,7 +178,11 @@ If foreign key constraints were originally enabled then run PRAGMA foreign_key_c addColumn(column: SQLiteManagerColumn): string { if (this.table.columns) { - this.table.columns.push(column) + if (typeof this.findColumn(column.name) == 'undefined') { + this.table.columns.push(column) + } else { + throw new Error('Column already exists') + } } else { this.table.columns = [column] } @@ -197,7 +204,11 @@ If foreign key constraints were originally enabled then run PRAGMA foreign_key_c const i = this.findColumn(oldColumnName) if (typeof i != 'undefined' && this.table.columns) { - this.table.columns[i].name = newColumnName + if (typeof this.findColumn(newColumnName) == 'undefined') { + this.table.columns[i].name = newColumnName + } else { + throw new Error('Column already exists') + } } return this.queryBuilder(AT.RENAME_COLUMN, { name: oldColumnName } as SQLiteManagerColumn, newColumnName) diff --git a/test/assets/manager-test-tables.ts b/test/assets/manager-test-tables.ts index 15dfc8b..3768fe2 100644 --- a/test/assets/manager-test-tables.ts +++ b/test/assets/manager-test-tables.ts @@ -57,7 +57,7 @@ export const testTable2 = { }, { name: 'test2', - type: types.SQLiteManagerType.INTEGER, + type: types.SQLiteManagerType.BLOB, constraints: { NOT_NULL: true, UNIQUE: true @@ -73,7 +73,7 @@ export const testTable2 = { }, { name: 'test4', - type: types.SQLiteManagerType.BLOB, + type: types.SQLiteManagerType.INTEGER, constraints: { NOT_NULL: true, UNIQUE: true, From 1c218cae394ebce6d4801bb003d541aabe57a189 Mon Sep 17 00:00:00 2001 From: Gioele Date: Thu, 1 Feb 2024 14:57:31 +0100 Subject: [PATCH 08/12] added: descriptions, column reference check, duplicate name check and a lot of fixes to the generic alter table --- src/manager.ts | 102 ++++++++++++++++++++++------- src/types.ts | 19 ++---- test/assets/manager-test-tables.ts | 16 +++-- 3 files changed, 97 insertions(+), 40 deletions(-) diff --git a/src/manager.ts b/src/manager.ts index 445b014..73684c2 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -7,10 +7,23 @@ import { SQLiteManagerDefault, SQLiteManagerCollate, SQLiteManagerForeignKeyOptions, - SQLiteManagerForeignKeyOn, - AT + SQLiteManagerForeignKeyOn } from './types' +enum AT { + ADD_COLUMN, + DROP_COLUMN, + RENAME_COLUMN, + RENAME_TABLE +} + +/** + * + * When creating a new istance of the SQLiteManager class, the constructor: + * - will get you in the alter table section if you pass an entire table + * - will get you to the create table section if you just pass the name of the table or you pass nothing + * + * */ export class SQLiteManager { private table: SQLiteManagerTable private create = false @@ -63,15 +76,33 @@ export class SQLiteManager { } else { if (typeof op != 'undefined' && column) { let oldname: string + switch (op) { case AT.RENAME_TABLE: this.query += 'ALTER TABLE "' + this.table.name + '" RENAME TO "' + column.name + '";\n' break case AT.ADD_COLUMN: - this.query += 'ALTER TABLE "' + this.table.name + '" ADD COLUMN ' + this.queryBuilderColumn(column) + ';\n' + if ( + column.constraints?.PRIMARY_KEY || + column.constraints?.UNIQUE || + column.constraints?.Default != SQLiteManagerDefault.NULL || + (typeof column.constraints.NOT_NULL != 'undefined' && column.constraints?.Default == SQLiteManagerDefault.NULL) || + (column.constraints?.ForeignKey?.enabled && + column.constraints?.ForeignKey?.table && + column.constraints?.ForeignKey?.column && + column.constraints?.Default != SQLiteManagerDefault.NULL) + ) { + query += this.queryBuilder('', {} as SQLiteManagerColumn) + } else { + this.query += 'ALTER TABLE "' + this.table.name + '" ADD COLUMN ' + this.queryBuilderColumn(column) + ';\n' + } break case AT.DROP_COLUMN: - this.query += 'ALTER TABLE "' + this.table.name + '" DROP COLUMN "' + column.name + '";\n' + if (this.isReferenced(column) || column.constraints?.PRIMARY_KEY || column.constraints?.UNIQUE) { + query += this.queryBuilder('', {} as SQLiteManagerColumn) + } else { + this.query += 'ALTER TABLE "' + this.table.name + '" DROP COLUMN "' + column.name + '";\n' + } break case AT.RENAME_COLUMN: if (newColumn) this.query += 'ALTER TABLE "' + this.table.name + '" RENAME COLUMN "' + column.name + '" TO "' + newColumn + '";\n' @@ -79,7 +110,10 @@ export class SQLiteManager { default: this.query += '\n\nPRAGMA foreign_keys = OFF;\n' this.query += 'BEGIN TRANSACTION;\n' - this.query += 'SELECT type, sql FROM sqlite_schema WHERE tbl_name=' + this.table.name + ';\n' + /* + this.query += 'WITH indexntriggernview AS (SELECT type, sql FROM sqlite_schema WHERE tbl_name="' + this.table.name + '");\n' + DROP views, indexes and triggers + */ this.create = true oldname = this.table.name if (typeof this.findColumn('new_' + this.table.name) == 'undefined') { @@ -93,13 +127,11 @@ export class SQLiteManager { this.query += 'DROP TABLE "' + oldname + '";\n' this.query += 'ALTER TABLE "' + this.table.name + '" RENAME TO "' + oldname + '";\n' this.table.name = oldname + this.query += 'CREATE INDEX ' /* -TODO -Use CREATE INDEX, CREATE TRIGGER, and CREATE VIEW to reconstruct indexes, triggers, and views associated with table X. Perhaps use the old format of the triggers, indexes, and views saved from step 3 above as a guide, making changes as appropriate for the alteration. - -If any views refer to table X in a way that is affected by the schema change, then drop those views using DROP VIEW and recreate them with whatever changes are necessary to accommodate the schema change using CREATE VIEW. - -*/ + Reconstruct using CREATE INDEX, CREATE TRIGGER, and CREATE VIEW + If any views refer to table X in a way that is affected by the schema change, then drop those views using DROP VIEW and recreate them with whatever changes are necessary to accommodate the schema change using CREATE VIEW. + */ this.query += 'PRAGMA foreign_key_check("' + this.table.name + '");\n' this.query += 'COMMIT;\n' this.query += 'PRAGMA foreign_keys = ON;\n' @@ -122,12 +154,17 @@ If any views refer to table X in a way that is affected by the schema change, th if (column.constraints) { const constraints: string[] = Object.keys(column.constraints).filter(key => { if (column.constraints) { - return column.constraints[key as keyof SQLiteManagerConstraints] + const constcol = column.constraints[key as keyof SQLiteManagerConstraints] + if (typeof constcol == 'boolean' && constcol == true) { + return constcol + } } }) constraints.forEach(constraint => { - query += ' ' + constraint.replace('_', ' ') + if (!(constraint == 'AUTOINCREMENT' && column.type != SQLiteManagerType.INTEGER)) { + query += ' ' + constraint.replace('_', ' ') + } }) if (column.constraints.Check) { @@ -155,27 +192,39 @@ If any views refer to table X in a way that is affected by the schema change, th if (column.constraints.ForeignKey) { if (column.constraints.ForeignKey.enabled) { query += ' REFERENCES ' + column.constraints.ForeignKey.table + '(' + column.constraints.ForeignKey.column + ')' - if (column.constraints.ForeignKey.options) { - query += ' ' + SQLiteManagerForeignKeyOptions[column.constraints.ForeignKey.options] - } - if (column.constraints.ForeignKey.onDelete) { - query += ' ON DELETE ' + SQLiteManagerForeignKeyOn[column.constraints.ForeignKey.onDelete] + if (typeof column.constraints.ForeignKey.onDelete !== 'undefined') { + query += ' ON DELETE ' + SQLiteManagerForeignKeyOn[column.constraints.ForeignKey.onDelete].replace('_', ' ') } - if (column.constraints.ForeignKey.onUpdate) { - query += ' ON UPDATE ' + SQLiteManagerForeignKeyOn[column.constraints.ForeignKey.onUpdate] + if (typeof column.constraints.ForeignKey.onUpdate !== 'undefined') { + query += ' ON UPDATE ' + SQLiteManagerForeignKeyOn[column.constraints.ForeignKey.onUpdate].replace('_', ' ') } if (column.constraints.ForeignKey.match) { query += ' MATCH ' + column.constraints.ForeignKey.match } + + if (typeof column.constraints.ForeignKey.options !== 'undefined') { + query += ' ' + SQLiteManagerForeignKeyOptions[column.constraints.ForeignKey.options].replace('_', ' ') + } } } } return query } + private isReferenced(column: SQLiteManagerColumn): boolean { + if (this.table.columns) { + for (let i = 0; i < this.table.columns.length; i++) { + if (this.table.columns[i].constraints?.ForeignKey?.column == column.name) { + return true + } + } + } + return false + } + addColumn(column: SQLiteManagerColumn): string { if (this.table.columns) { if (typeof this.findColumn(column.name) == 'undefined') { @@ -191,13 +240,20 @@ If any views refer to table X in a way that is affected by the schema change, th } deleteColumn(name: string): string { + let query = '' const i = this.findColumn(name) - if (typeof i != 'undefined' && this.table.columns) { - this.table.columns.splice(i, 1) + if (this.table.columns && typeof i != 'undefined') { + if (this.create) { + this.table.columns.splice(i, 1) + query = this.queryBuilder(AT.DROP_COLUMN, this.table.columns[i]) + } else { + query = this.queryBuilder(AT.DROP_COLUMN, this.table.columns[i]) + this.table.columns.splice(i, 1) + } } - return this.queryBuilder(AT.DROP_COLUMN, { name: name } as SQLiteManagerColumn) + return query } renameColumn(oldColumnName: string, newColumnName: string): string { diff --git a/src/types.ts b/src/types.ts index a975ebb..77a5673 100644 --- a/src/types.ts +++ b/src/types.ts @@ -175,7 +175,7 @@ export enum SQLiteManagerForeignKeyOn { } /** SQLite foreign key */ -class SQLiteManagerForeignKey { +export class SQLiteManagerForeignKey { enabled = false table = '' column = '' @@ -201,7 +201,7 @@ class SQLiteManagerForeignKey { } /** By disabling foreign key you delete references */ - disable() { + disable(): void { this.enabled = false this.table = '' this.column = '' @@ -218,7 +218,7 @@ class SQLiteManagerForeignKey { onDelete?: SQLiteManagerForeignKeyOn, onUpdate?: SQLiteManagerForeignKeyOn, match?: string - ) { + ): void { this.enabled = true this.table = table this.column = column @@ -249,12 +249,13 @@ export interface SQLiteManagerConstraints { UNIQUE?: boolean Check?: string - /** You should pass: SQLiteManagerDefault.NULL, SQLiteManagerDefault.CURRENT_TIME, etc. */ + /** You can leave it undefined or pass: SQLiteManagerDefault.NULL, SQLiteManagerDefault.CURRENT_TIME, etc. */ Default?: SQLiteManagerDefault | string - /** You should pass: SQLiteManagerCollate.BINARY, SQLiteManagerCollate.NOCASE, etc. */ + /** You can leave it undefined or pass: SQLiteManagerCollate.BINARY, SQLiteManagerCollate.NOCASE, etc. */ Collate?: SQLiteManagerCollate | string + /** You should pass: new SQLiteManagerForeignKey(), or you can leave it undefined */ ForeignKey?: SQLiteManagerForeignKey } @@ -278,11 +279,3 @@ export interface SQLiteManagerTable { /** Columns */ columns?: SQLiteManagerColumn[] } - -/** SQLite Alter Table */ -export enum AT { - ADD_COLUMN, - DROP_COLUMN, - RENAME_COLUMN, - RENAME_TABLE -} diff --git a/test/assets/manager-test-tables.ts b/test/assets/manager-test-tables.ts index 3768fe2..9682798 100644 --- a/test/assets/manager-test-tables.ts +++ b/test/assets/manager-test-tables.ts @@ -49,10 +49,10 @@ export const testTable2 = { name: 'test1', type: types.SQLiteManagerType.REAL, constraints: { - PRIMARY_KEY: true, + PRIMARY_KEY: false, AUTOINCREMENT: true, - NOT_NULL: true, - UNIQUE: true + UNIQUE: false, + Default: types.SQLiteManagerDefault.NULL } }, { @@ -77,7 +77,15 @@ export const testTable2 = { constraints: { NOT_NULL: true, UNIQUE: true, - CHECK: 'test4 > 0' + CHECK: 'test4 > 0', + ForeignKey: new types.SQLiteManagerForeignKey( + true, + 'myTable2', + 'test1', + types.SQLiteManagerForeignKeyOptions.NONE, + types.SQLiteManagerForeignKeyOn.CASCADE, + types.SQLiteManagerForeignKeyOn.CASCADE + ) } } ] From 9d8dd0c5eab65cca7373ab0a3f7e1d14c7cbedf2 Mon Sep 17 00:00:00 2001 From: Gioele Date: Fri, 2 Feb 2024 21:26:08 +0100 Subject: [PATCH 09/12] +: methods description, finished alter table's checks with sqlite_schema that's now asked to the user, multiple case of duplicated code in a single function; tbf with tests on a real db --- src/manager.ts | 117 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 34 deletions(-) diff --git a/src/manager.ts b/src/manager.ts index 73684c2..28e1bce 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -22,12 +22,14 @@ enum AT { * When creating a new istance of the SQLiteManager class, the constructor: * - will get you in the alter table section if you pass an entire table * - will get you to the create table section if you just pass the name of the table or you pass nothing + * IMPORTANT: if you don't call sqlite_schema before making any change, then old views, triggers and indexes will be lost * * */ export class SQLiteManager { private table: SQLiteManagerTable private create = false private query = '' + private sql: string[] = [] constructor(table?: SQLiteManagerTable) { if (typeof table === 'undefined') { @@ -44,8 +46,9 @@ export class SQLiteManager { } } - reset(): void { - this.table = {} as SQLiteManagerTable + /** Pass to this method the result of this query: SELECT sql FROM sqlite_schema WHERE tbl_name='X'; where X is the name of the table you're using */ + sqlite_schema(sql: string[]): void { + this.sql = sql } /** If changing name in altertable you need to manually call the queryBuilder() */ @@ -98,8 +101,15 @@ export class SQLiteManager { } break case AT.DROP_COLUMN: - if (this.isReferenced(column) || column.constraints?.PRIMARY_KEY || column.constraints?.UNIQUE) { - query += this.queryBuilder('', {} as SQLiteManagerColumn) + if ( + this.is(column, true) || + column.constraints?.PRIMARY_KEY || + column.constraints?.UNIQUE || + this.sql?.includes(column.name) || + this.is(column, false, true) + // can't check for generated columns and outside this table CHECK constraints + ) { + query += this.queryBuilder('' + AT[op], column) } else { this.query += 'ALTER TABLE "' + this.table.name + '" DROP COLUMN "' + column.name + '";\n' } @@ -110,10 +120,6 @@ export class SQLiteManager { default: this.query += '\n\nPRAGMA foreign_keys = OFF;\n' this.query += 'BEGIN TRANSACTION;\n' - /* - this.query += 'WITH indexntriggernview AS (SELECT type, sql FROM sqlite_schema WHERE tbl_name="' + this.table.name + '");\n' - DROP views, indexes and triggers - */ this.create = true oldname = this.table.name if (typeof this.findColumn('new_' + this.table.name) == 'undefined') { @@ -128,10 +134,21 @@ export class SQLiteManager { this.query += 'ALTER TABLE "' + this.table.name + '" RENAME TO "' + oldname + '";\n' this.table.name = oldname this.query += 'CREATE INDEX ' - /* - Reconstruct using CREATE INDEX, CREATE TRIGGER, and CREATE VIEW - If any views refer to table X in a way that is affected by the schema change, then drop those views using DROP VIEW and recreate them with whatever changes are necessary to accommodate the schema change using CREATE VIEW. - */ + + if (this.sql) { + if (op == 'DROP_COLUMN' && column) { + this.sql.forEach(element => { + if (!element.includes(column.name)) { + query += element + '\n' + } + }) + } else { + this.sql.forEach(element => { + query += element + '\n' + }) + } + } + this.query += 'PRAGMA foreign_key_check("' + this.table.name + '");\n' this.query += 'COMMIT;\n' this.query += 'PRAGMA foreign_keys = ON;\n' @@ -214,18 +231,11 @@ export class SQLiteManager { return query } - private isReferenced(column: SQLiteManagerColumn): boolean { - if (this.table.columns) { - for (let i = 0; i < this.table.columns.length; i++) { - if (this.table.columns[i].constraints?.ForeignKey?.column == column.name) { - return true - } - } - } - return false - } - - addColumn(column: SQLiteManagerColumn): string { + /** + * column: the SQLiteManagerColumn you want to add to the table + * sql[]: SELECT sql FROM sqlite_schema WHERE tbl_name='X'; where X is the name of the table you're using + */ + addColumn(column: SQLiteManagerColumn, sql: string[]): string { if (this.table.columns) { if (typeof this.findColumn(column.name) == 'undefined') { this.table.columns.push(column) @@ -236,10 +246,15 @@ export class SQLiteManager { this.table.columns = [column] } + this.sqlite_schema(sql) return this.queryBuilder(AT.ADD_COLUMN, column) } - deleteColumn(name: string): string { + /** + * name: name of the column you want to delete + * sql[]: SELECT sql FROM sqlite_schema WHERE tbl_name='X'; where X is the name of the table you're using + */ + deleteColumn(name: string, sql: string[]): string { let query = '' const i = this.findColumn(name) @@ -253,6 +268,7 @@ export class SQLiteManager { } } + this.sqlite_schema(sql) return query } @@ -270,24 +286,43 @@ export class SQLiteManager { return this.queryBuilder(AT.RENAME_COLUMN, { name: oldColumnName } as SQLiteManagerColumn, newColumnName) } - changeColumnType(name: string, type: SQLiteManagerType): string { - const i = this.findColumn(name) + /** + * name: name of the column you want to change the type of + * type: the new type you want to give to the column + * sql[]: SELECT sql FROM sqlite_schema WHERE tbl_name='X'; where X is the name of the table you're using + */ + changeColumnType(name: string, type: SQLiteManagerType, sql: string[]): string { + return this.generalFun(name, (column: SQLiteManagerColumn) => (column.type = type), sql) + } - if (typeof i != 'undefined' && this.table.columns) { - this.table.columns[i].type = type - } + /** + * name: name of the column you want to change constraints of + * constraits: edited constraints you get from getConstraints() + * sql[]: SELECT sql FROM sqlite_schema WHERE tbl_name='X'; where X is the name of the table you're using + */ + changeColumnConstraints(name: string, constraints: SQLiteManagerConstraints, sql: string[]): string { + return this.generalFun(name, (column: SQLiteManagerColumn) => (column.constraints = constraints), sql) + } - return this.queryBuilder('', {} as SQLiteManagerColumn) + /** name: name of the column you want to get the constraints of */ + getConstraints(name: string): SQLiteManagerConstraints | undefined { + let rtconstraints + this.generalFun(name, (column: SQLiteManagerColumn) => (rtconstraints = column.constraints)) + return rtconstraints } - changeColumnConstraints(name: string, constraints: SQLiteManagerConstraints): string { + private generalFun(name: string, fun: (column: SQLiteManagerColumn) => void, sql?: string[], qb1?: any, qb2?: SQLiteManagerColumn): string { const i = this.findColumn(name) if (typeof i != 'undefined' && this.table.columns) { - this.table.columns[i].constraints = constraints + fun(this.table.columns[i]) } - return this.queryBuilder('', {} as SQLiteManagerColumn) + if (sql) { + this.sqlite_schema(sql) + } + + return this.queryBuilder(qb1 ? qb1 : '', qb2 ? qb2 : ({} as SQLiteManagerColumn)) } private findColumn(name: string): number | undefined { @@ -298,4 +333,18 @@ export class SQLiteManager { } } } + + private is(column: SQLiteManagerColumn, referenced?: boolean, checked?: boolean): boolean { + if (this.table.columns) { + for (let i = 0; i < this.table.columns.length; i++) { + if (referenced && this.table.columns[i].constraints?.ForeignKey?.column == column.name) { + return true + } + if (checked && this.table.columns[i].constraints?.Check?.includes(column.name)) { + return true + } + } + } + return false + } } From 219a50b3a6b8ed5b39cace6337df012e61c08feb Mon Sep 17 00:00:00 2001 From: Gioele Date: Mon, 5 Feb 2024 11:22:51 +0100 Subject: [PATCH 10/12] added sqlite_schema tests, and various fixes --- src/manager.ts | 27 +++++++++++++++------------ test/assets/manager-test-tables.ts | 6 ++++++ test/manager.test.ts | 16 ++++++++++------ 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/manager.ts b/src/manager.ts index 28e1bce..7b821b3 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -47,8 +47,14 @@ export class SQLiteManager { } /** Pass to this method the result of this query: SELECT sql FROM sqlite_schema WHERE tbl_name='X'; where X is the name of the table you're using */ - sqlite_schema(sql: string[]): void { - this.sql = sql + sqlite_schema(sql?: string[]): void { + if (sql) { + this.sql = sql + } else { + if (!this.create) { + throw new Error('Alter table needs SELECT sql FROM sqlite_schema WHERE tbl_name=X; as sql') + } + } } /** If changing name in altertable you need to manually call the queryBuilder() */ @@ -133,18 +139,17 @@ export class SQLiteManager { this.query += 'DROP TABLE "' + oldname + '";\n' this.query += 'ALTER TABLE "' + this.table.name + '" RENAME TO "' + oldname + '";\n' this.table.name = oldname - this.query += 'CREATE INDEX ' if (this.sql) { if (op == 'DROP_COLUMN' && column) { this.sql.forEach(element => { if (!element.includes(column.name)) { - query += element + '\n' + this.query += element + '\n' } }) } else { this.sql.forEach(element => { - query += element + '\n' + this.query += element + '\n' }) } } @@ -235,7 +240,7 @@ export class SQLiteManager { * column: the SQLiteManagerColumn you want to add to the table * sql[]: SELECT sql FROM sqlite_schema WHERE tbl_name='X'; where X is the name of the table you're using */ - addColumn(column: SQLiteManagerColumn, sql: string[]): string { + addColumn(column: SQLiteManagerColumn, sql?: string[]): string { if (this.table.columns) { if (typeof this.findColumn(column.name) == 'undefined') { this.table.columns.push(column) @@ -254,7 +259,7 @@ export class SQLiteManager { * name: name of the column you want to delete * sql[]: SELECT sql FROM sqlite_schema WHERE tbl_name='X'; where X is the name of the table you're using */ - deleteColumn(name: string, sql: string[]): string { + deleteColumn(name: string, sql?: string[]): string { let query = '' const i = this.findColumn(name) @@ -291,7 +296,7 @@ export class SQLiteManager { * type: the new type you want to give to the column * sql[]: SELECT sql FROM sqlite_schema WHERE tbl_name='X'; where X is the name of the table you're using */ - changeColumnType(name: string, type: SQLiteManagerType, sql: string[]): string { + changeColumnType(name: string, type: SQLiteManagerType, sql?: string[]): string { return this.generalFun(name, (column: SQLiteManagerColumn) => (column.type = type), sql) } @@ -300,7 +305,7 @@ export class SQLiteManager { * constraits: edited constraints you get from getConstraints() * sql[]: SELECT sql FROM sqlite_schema WHERE tbl_name='X'; where X is the name of the table you're using */ - changeColumnConstraints(name: string, constraints: SQLiteManagerConstraints, sql: string[]): string { + changeColumnConstraints(name: string, constraints: SQLiteManagerConstraints, sql?: string[]): string { return this.generalFun(name, (column: SQLiteManagerColumn) => (column.constraints = constraints), sql) } @@ -318,9 +323,7 @@ export class SQLiteManager { fun(this.table.columns[i]) } - if (sql) { - this.sqlite_schema(sql) - } + this.sqlite_schema(sql) return this.queryBuilder(qb1 ? qb1 : '', qb2 ? qb2 : ({} as SQLiteManagerColumn)) } diff --git a/test/assets/manager-test-tables.ts b/test/assets/manager-test-tables.ts index 9682798..d1c6f31 100644 --- a/test/assets/manager-test-tables.ts +++ b/test/assets/manager-test-tables.ts @@ -90,3 +90,9 @@ export const testTable2 = { } ] } + +export const sqlite_schema = [ + 'CREATE VIEW "myView" AS SELECT "myTable"."column1", "myTable"."column2", "myTable"."column3", "myTable"."column4" FROM "myTable";', + 'CREATE TRIGGER "myTrigger" AFTER INSERT ON "myTable" BEGIN INSERT INTO "myTable2" ("test1", "test2", "test3", "test4") VALUES (new."column1", new."column2", new."column3", new."column4"); END;', + 'CREATE INDEX "myIndex" ON "myTable" ("column1", "column2", "column3", "column4");' +] diff --git a/test/manager.test.ts b/test/manager.test.ts index c0fbf95..980c2a5 100644 --- a/test/manager.test.ts +++ b/test/manager.test.ts @@ -1,7 +1,7 @@ /* eslint-disable prettier/prettier */ import { SQLiteManager } from '../src/manager' import { SQLiteManagerType } from '../src/types' -import { testTable, testTable2 } from './assets/manager-test-tables' +import { testTable, testTable2, sqlite_schema } from './assets/manager-test-tables' describe('Create a table', () => { let manager: SQLiteManager @@ -34,7 +34,7 @@ describe('Create a table', () => { it('tests alter table', () => { manager = new SQLiteManager(testTable) - const addColumn: string = manager.addColumn(testTable2.columns[0]) + const addColumn: string = manager.addColumn(testTable2.columns[0], sqlite_schema) expect(addColumn).toContain('ALTER TABLE') expect(addColumn).toContain(testTable.name) @@ -42,8 +42,10 @@ describe('Create a table', () => { expect(addColumn).toContain(testTable2.columns[0].name) expect(addColumn).toContain(SQLiteManagerType[testTable2.columns[0].type]) - manager.addColumn(JSON.parse(JSON.stringify(testTable2.columns[1]))) - expect(manager.deleteColumn(testTable2.columns[0].name)).toContain('ALTER TABLE "' + testTable.name + '" DROP COLUMN "' + testTable2.columns[0].name + '";') + manager.addColumn(JSON.parse(JSON.stringify(testTable2.columns[1])), sqlite_schema) + expect(manager.deleteColumn(testTable2.columns[0].name, sqlite_schema)).toContain( + 'ALTER TABLE "' + testTable.name + '" DROP COLUMN "' + testTable2.columns[0].name + '";' + ) const risRen: string = manager.renameColumn(testTable.columns[1].name, testTable2.columns[2].name) @@ -55,7 +57,7 @@ describe('Create a table', () => { expect(renTable).toContain('ALTER TABLE "' + testTable.name + '" RENAME TO "' + testTable2.name + '";') - const risCh: string = manager.changeColumnType(testTable.columns[2].name, SQLiteManagerType.TEXT) + const risCh: string = manager.changeColumnType(testTable.columns[2].name, SQLiteManagerType.TEXT, sqlite_schema) expect(risCh).toContain('PRAGMA foreign_keys = OFF;') expect(risCh).toContain('BEGIN TRANSACTION;') @@ -66,7 +68,9 @@ describe('Create a table', () => { expect(risCh).toContain('COMMIT;') expect(risCh).toContain('PRAGMA foreign_keys = ON;') - const risCnstr: string = manager.changeColumnConstraints(testTable.columns[2].name, testTable2.columns[2].constraints) + const risCnstr: string = manager.changeColumnConstraints(testTable.columns[2].name, testTable2.columns[2].constraints, sqlite_schema) expect(risCnstr).toContain('"' + testTable.columns[2].name + '" TEXT UNIQUE') + + console.log(risCnstr) }) }) From 3d7b497b3bf1dda7be3d900afbd7f84413bf422b Mon Sep 17 00:00:00 2001 From: Gioele Date: Mon, 5 Feb 2024 19:13:33 +0100 Subject: [PATCH 11/12] + name escape --- src/manager.ts | 35 ++++++++-- src/types.ts | 150 +++++++++++++++++++++++++++++++++++++++++++ test/manager.test.ts | 4 +- 3 files changed, 181 insertions(+), 8 deletions(-) diff --git a/src/manager.ts b/src/manager.ts index 7b821b3..538d9dd 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -7,7 +7,8 @@ import { SQLiteManagerDefault, SQLiteManagerCollate, SQLiteManagerForeignKeyOptions, - SQLiteManagerForeignKeyOn + SQLiteManagerForeignKeyOn, + keywords } from './types' enum AT { @@ -40,9 +41,14 @@ export class SQLiteManager { this.create = true if (table.columns) { this.create = false + this.table = table + } else { + this.table = { name: this.escape(table.name) } as SQLiteManagerTable } + } else { + this.create = true + this.table = {} as SQLiteManagerTable } - this.table = table } } @@ -60,10 +66,10 @@ export class SQLiteManager { /** If changing name in altertable you need to manually call the queryBuilder() */ set name(name: string) { if (this.create) { - this.table.name = name + this.table.name = this.escape(name) } else { - this.table.name = name - this.queryBuilder(AT.RENAME_TABLE, { name: name } as SQLiteManagerColumn) + this.table.name = this.escape(name) + this.queryBuilder(AT.RENAME_TABLE, { name: this.table.name } as SQLiteManagerColumn) } } @@ -241,6 +247,7 @@ export class SQLiteManager { * sql[]: SELECT sql FROM sqlite_schema WHERE tbl_name='X'; where X is the name of the table you're using */ addColumn(column: SQLiteManagerColumn, sql?: string[]): string { + column.name = this.escape(column.name) if (this.table.columns) { if (typeof this.findColumn(column.name) == 'undefined') { this.table.columns.push(column) @@ -279,6 +286,8 @@ export class SQLiteManager { renameColumn(oldColumnName: string, newColumnName: string): string { const i = this.findColumn(oldColumnName) + newColumnName = this.escape(newColumnName) + let query = '' if (typeof i != 'undefined' && this.table.columns) { if (typeof this.findColumn(newColumnName) == 'undefined') { @@ -286,9 +295,10 @@ export class SQLiteManager { } else { throw new Error('Column already exists') } + query = this.queryBuilder(AT.RENAME_COLUMN, { name: oldColumnName } as SQLiteManagerColumn, newColumnName) } - return this.queryBuilder(AT.RENAME_COLUMN, { name: oldColumnName } as SQLiteManagerColumn, newColumnName) + return query } /** @@ -350,4 +360,17 @@ export class SQLiteManager { } return false } + + private escape(name: string): string { + keywords.forEach(keyword => { + if (name.toUpperCase() == keyword) { + throw new Error("You can't use a SQLite keyword as a name") + } + }) + + name = name.replace(/'/g, "''") + name = name.replace(/"/g, '""') + + return name + } } diff --git a/src/types.ts b/src/types.ts index 77a5673..bf8c624 100644 --- a/src/types.ts +++ b/src/types.ts @@ -279,3 +279,153 @@ export interface SQLiteManagerTable { /** Columns */ columns?: SQLiteManagerColumn[] } + +export const keywords = [ + 'ABORT', + 'ACTION', + 'ADD', + 'AFTER', + 'ALL', + 'ALTER', + 'ALWAYS', + 'ANALYZE', + 'AND', + 'AS', + 'ASC', + 'ATTACH', + 'AUTOINCREMENT', + 'BEFORE', + 'BEGIN', + 'BETWEEN', + 'BY', + 'CASCADE', + 'CASE', + 'CAST', + 'CHECK', + 'COLLATE', + 'COLUMN', + 'COMMIT', + 'CONFLICT', + 'CONSTRAINT', + 'CREATE', + 'CROSS', + 'CURRENT', + 'CURRENT_DATE', + 'CURRENT_TIME', + 'CURRENT_TIMESTAMP', + 'DATABASE', + 'DEFAULT', + 'DEFERRABLE', + 'DEFERRED', + 'DELETE', + 'DESC', + 'DETACH', + 'DISTINCT', + 'DO', + 'DROP', + 'EACH', + 'ELSE', + 'END', + 'ESCAPE', + 'EXCEPT', + 'EXCLUDE', + 'EXCLUSIVE', + 'EXISTS', + 'EXPLAIN', + 'FAIL', + 'FILTER', + 'FIRST', + 'FOLLOWING', + 'FOR', + 'FOREIGN', + 'FROM', + 'FULL', + 'GENERATED', + 'GLOB', + 'GROUP', + 'GROUPS', + 'HAVING', + 'IF', + 'IGNORE', + 'IMMEDIATE', + 'IN', + 'INDEX', + 'INDEXED', + 'INITIALLY', + 'INNER', + 'INSERT', + 'INSTEAD', + 'INTERSECT', + 'INTO', + 'IS', + 'ISNULL', + 'JOIN', + 'KEY', + 'LAST', + 'LEFT', + 'LIKE', + 'LIMIT', + 'MATCH', + 'MATERIALIZED', + 'NATURAL', + 'NO', + 'NOT', + 'NOTHING', + 'NOTNULL', + 'NULL', + 'NULLS', + 'OF', + 'OFFSET', + 'ON', + 'OR', + 'ORDER', + 'OTHERS', + 'OUTER', + 'OVER', + 'PARTITION', + 'PLAN', + 'PRAGMA', + 'PRECEDING', + 'PRIMARY', + 'QUERY', + 'RAISE', + 'RANGE', + 'RECURSIVE', + 'REFERENCES', + 'REGEXP', + 'REINDEX', + 'RELEASE', + 'RENAME', + 'REPLACE', + 'RESTRICT', + 'RETURNING', + 'RIGHT', + 'ROLLBACK', + 'ROW', + 'ROWS', + 'SAVEPOINT', + 'SELECT', + 'SET', + 'TABLE', + 'TEMP', + 'TEMPORARY', + 'THEN', + 'TIES', + 'TO', + 'TRANSACTION', + 'TRIGGER', + 'UNBOUNDED', + 'UNION', + 'UNIQUE', + 'UPDATE', + 'USING', + 'VACUUM', + 'VALUES', + 'VIEW', + 'VIRTUAL', + 'WHEN', + 'WHERE', + 'WINDOW', + 'WITH', + 'WITHOUT' +] diff --git a/test/manager.test.ts b/test/manager.test.ts index 980c2a5..fea1bfd 100644 --- a/test/manager.test.ts +++ b/test/manager.test.ts @@ -17,6 +17,7 @@ describe('Create a table', () => { expect(manager.deleteColumn(testTable.columns[0].name)).not.toContain(testTable.columns[0].name) const risRen: string = manager.renameColumn(testTable.columns[1].name, testTable.columns[2].name) + console.log(risRen) expect(risRen).not.toContain(testTable.columns[1].name) expect(risRen).toContain(testTable.columns[2].name) @@ -29,6 +30,7 @@ describe('Create a table', () => { const risCnstr: string = manager.changeColumnConstraints(testTable.columns[2].name, testTable.columns[2].constraints) expect(risCnstr).toContain('NOT NULL UNIQUE') + console.log(risCnstr) }) it('tests alter table', () => { @@ -70,7 +72,5 @@ describe('Create a table', () => { const risCnstr: string = manager.changeColumnConstraints(testTable.columns[2].name, testTable2.columns[2].constraints, sqlite_schema) expect(risCnstr).toContain('"' + testTable.columns[2].name + '" TEXT UNIQUE') - - console.log(risCnstr) }) }) From be0b7eecd64cf795c3243c83eaf0d8421d459c56 Mon Sep 17 00:00:00 2001 From: Gioele Date: Tue, 6 Feb 2024 20:56:14 +0100 Subject: [PATCH 12/12] now constraints of a table can be changed just by passing the single edits; added getCompatibleConstraints to get all compatible constraints by passing a column or its type --- src/manager.ts | 48 ++++++++++++++++++++++++++++++++++++++------ test/manager.test.ts | 6 ++---- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/manager.ts b/src/manager.ts index 538d9dd..2dc9465 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -18,6 +18,11 @@ enum AT { RENAME_TABLE } +interface constraints { + constrain: string + active: boolean +} + /** * * When creating a new istance of the SQLiteManager class, the constructor: @@ -316,21 +321,52 @@ export class SQLiteManager { * sql[]: SELECT sql FROM sqlite_schema WHERE tbl_name='X'; where X is the name of the table you're using */ changeColumnConstraints(name: string, constraints: SQLiteManagerConstraints, sql?: string[]): string { - return this.generalFun(name, (column: SQLiteManagerColumn) => (column.constraints = constraints), sql) + return this.generalFun(name, (column: SQLiteManagerColumn) => (column.constraints = { ...column.constraints, ...constraints }), sql) } - /** name: name of the column you want to get the constraints of */ - getConstraints(name: string): SQLiteManagerConstraints | undefined { - let rtconstraints - this.generalFun(name, (column: SQLiteManagerColumn) => (rtconstraints = column.constraints)) - return rtconstraints + /** name: name of the column OR name of the type you want to get the compatible constraints of */ + getCompatibleConstraints(name: string | SQLiteManagerType): constraints[] { + const constraints: constraints[] = [] + let constrain: constraints + + const cns = ['PRIMARY_KEY', 'AUTOINCREMENT', 'NOT_NULL', 'UNIQUE', 'Check', 'Default', 'Collate', 'ForeignKey'] + + this.create = true + + if (typeof name == 'string') { + this.generalFun(name, (column: SQLiteManagerColumn) => + cns.forEach(key => { + constrain = { constrain: key, active: true } + if (column.type != SQLiteManagerType.INTEGER && key == 'AUTOINCREMENT') { + constrain.active = false + } + constraints.push(constrain) + }) + ) + } else { + cns.forEach(key => { + constrain = { constrain: key, active: true } + if (name != SQLiteManagerType.INTEGER && key == 'AUTOINCREMENT') { + constrain.active = false + } + constraints.push(constrain) + }) + } + + this.create = false + + return constraints } private generalFun(name: string, fun: (column: SQLiteManagerColumn) => void, sql?: string[], qb1?: any, qb2?: SQLiteManagerColumn): string { const i = this.findColumn(name) if (typeof i != 'undefined' && this.table.columns) { + /* console.log('IN: ') //DEBUG + console.log(this.table.columns[i]) */ fun(this.table.columns[i]) + /* console.log('OUT: ') //DEBUG + console.log(this.table.columns[i]) */ } this.sqlite_schema(sql) diff --git a/test/manager.test.ts b/test/manager.test.ts index fea1bfd..9c799aa 100644 --- a/test/manager.test.ts +++ b/test/manager.test.ts @@ -17,7 +17,6 @@ describe('Create a table', () => { expect(manager.deleteColumn(testTable.columns[0].name)).not.toContain(testTable.columns[0].name) const risRen: string = manager.renameColumn(testTable.columns[1].name, testTable.columns[2].name) - console.log(risRen) expect(risRen).not.toContain(testTable.columns[1].name) expect(risRen).toContain(testTable.columns[2].name) @@ -27,10 +26,9 @@ describe('Create a table', () => { expect(risCh).toContain(SQLiteManagerType[SQLiteManagerType.TEXT]) expect(risCh).not.toContain(SQLiteManagerType[SQLiteManagerType.INTEGER]) - const risCnstr: string = manager.changeColumnConstraints(testTable.columns[2].name, testTable.columns[2].constraints) + const risCnstr: string = manager.changeColumnConstraints(testTable.columns[2].name, { NOT_NULL: false }) - expect(risCnstr).toContain('NOT NULL UNIQUE') - console.log(risCnstr) + expect(risCnstr).toContain('UNIQUE') }) it('tests alter table', () => {