rake-db 2.2.4 → 2.2.6

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.
@@ -10,7 +10,12 @@ import {
10
10
  toArray,
11
11
  } from 'pqb';
12
12
  import { ColumnComment, Migration } from './migration';
13
- import { joinColumns, joinWords, quoteTable } from '../common';
13
+ import {
14
+ getSchemaAndTableFromName,
15
+ joinColumns,
16
+ joinWords,
17
+ quoteWithSchema,
18
+ } from '../common';
14
19
 
15
20
  export const columnToSql = (
16
21
  key: string,
@@ -30,7 +35,7 @@ export const columnToSql = (
30
35
 
31
36
  if (item.isPrimaryKey && !hasMultiplePrimaryKeys) {
32
37
  line.push('PRIMARY KEY');
33
- } else if (!item.isNullable) {
38
+ } else if (!item.data.isNullable) {
34
39
  line.push('NOT NULL');
35
40
  }
36
41
 
@@ -48,7 +53,7 @@ export const columnToSql = (
48
53
 
49
54
  const { foreignKey } = item.data;
50
55
  if (foreignKey) {
51
- const table = getForeignKeyTable(
56
+ const [schema, table] = getForeignKeyTable(
52
57
  'fn' in foreignKey ? foreignKey.fn : foreignKey.table,
53
58
  );
54
59
 
@@ -56,7 +61,7 @@ export const columnToSql = (
56
61
  line.push(`CONSTRAINT "${foreignKey.name}"`);
57
62
  }
58
63
 
59
- line.push(referencesToSql(table, foreignKey.columns, foreignKey));
64
+ line.push(referencesToSql(schema, table, foreignKey.columns, foreignKey));
60
65
  }
61
66
 
62
67
  return line.join(' ');
@@ -89,42 +94,49 @@ export const addColumnComment = (
89
94
 
90
95
  export const getForeignKeyTable = (
91
96
  fnOrTable: (() => ForeignKeyTable) | string,
92
- ) => {
97
+ ): [string | undefined, string] => {
93
98
  if (typeof fnOrTable === 'string') {
94
- return fnOrTable;
99
+ return getSchemaAndTableFromName(fnOrTable);
95
100
  }
96
101
 
97
- const klass = fnOrTable();
98
- return new klass().table;
102
+ const item = new (fnOrTable())();
103
+ return [item.schema, item.table];
99
104
  };
100
105
 
101
106
  export const constraintToSql = (
102
- tableName: string,
107
+ { name }: { schema?: string; name: string },
103
108
  up: boolean,
104
109
  foreignKey: TableData['foreignKeys'][number],
105
110
  ) => {
106
111
  const constraintName =
107
- foreignKey.options.name ||
108
- `${tableName}_${foreignKey.columns.join('_')}_fkey`;
112
+ foreignKey.options.name || `${name}_${foreignKey.columns.join('_')}_fkey`;
109
113
 
110
114
  if (!up) {
111
115
  const { dropMode } = foreignKey.options;
112
116
  return `CONSTRAINT "${constraintName}"${dropMode ? ` ${dropMode}` : ''}`;
113
117
  }
114
118
 
115
- const table = getForeignKeyTable(foreignKey.fnOrTable);
119
+ const [schema, table] = getForeignKeyTable(foreignKey.fnOrTable);
116
120
  return `CONSTRAINT "${constraintName}" FOREIGN KEY (${joinColumns(
117
121
  foreignKey.columns,
118
- )}) ${referencesToSql(table, foreignKey.foreignColumns, foreignKey.options)}`;
122
+ )}) ${referencesToSql(
123
+ schema,
124
+ table,
125
+ foreignKey.foreignColumns,
126
+ foreignKey.options,
127
+ )}`;
119
128
  };
120
129
 
121
130
  export const referencesToSql = (
131
+ schema: string | undefined,
122
132
  table: string,
123
133
  columns: string[],
124
134
  foreignKey: Pick<ForeignKeyOptions, 'match' | 'onDelete' | 'onUpdate'>,
125
135
  ) => {
126
136
  const sql: string[] = [
127
- `REFERENCES ${quoteTable(table)}(${joinColumns(columns)})`,
137
+ `REFERENCES ${quoteWithSchema({ schema, name: table })}(${joinColumns(
138
+ columns,
139
+ )})`,
128
140
  ];
129
141
 
130
142
  if (foreignKey.match) {
@@ -144,13 +156,13 @@ export const referencesToSql = (
144
156
 
145
157
  export const indexesToQuery = (
146
158
  up: boolean,
147
- tableName: string,
159
+ { schema, name }: { schema?: string; name: string },
148
160
  indexes: TableData.Index[],
149
161
  ): Sql[] => {
150
162
  return indexes.map(({ columns, options }) => {
151
163
  const indexName =
152
164
  options.name ||
153
- joinWords(tableName, ...columns.map(({ column }) => column), 'index');
165
+ joinWords(name, ...columns.map(({ column }) => column), 'index');
154
166
 
155
167
  if (!up) {
156
168
  return {
@@ -169,7 +181,7 @@ export const indexesToQuery = (
169
181
  sql.push('UNIQUE');
170
182
  }
171
183
 
172
- sql.push(`INDEX "${indexName}" ON ${quoteTable(tableName)}`);
184
+ sql.push(`INDEX "${indexName}" ON ${quoteWithSchema({ schema, name })}`);
173
185
 
174
186
  if (options.using) {
175
187
  sql.push(`USING ${options.using}`);
@@ -234,13 +246,13 @@ export const indexesToQuery = (
234
246
  };
235
247
 
236
248
  export const commentsToQuery = (
237
- tableName: string,
249
+ schemaTable: { schema?: string; name: string },
238
250
  comments: ColumnComment[],
239
251
  ): Sql[] => {
240
252
  return comments.map(({ column, comment }) => ({
241
- text: `COMMENT ON COLUMN ${quoteTable(tableName)}."${column}" IS ${quote(
242
- comment,
243
- )}`,
253
+ text: `COMMENT ON COLUMN ${quoteWithSchema(
254
+ schemaTable,
255
+ )}."${column}" IS ${quote(comment)}`,
244
256
  values: [],
245
257
  }));
246
258
  };
@@ -0,0 +1,158 @@
1
+ import { DbStructure } from './dbStructure';
2
+ import { Adapter } from 'pqb';
3
+
4
+ const adapter = new Adapter({
5
+ databaseURL: 'file:path',
6
+ });
7
+ let rows: unknown[][] | Record<string, unknown>[] = [];
8
+ adapter.query = jest.fn().mockImplementation(() => ({ rows }));
9
+ adapter.arrays = jest.fn().mockImplementation(() => ({ rows }));
10
+ const db = new DbStructure(adapter);
11
+
12
+ describe('dbStructure', () => {
13
+ describe('getSchemas', () => {
14
+ it('should return schemas', async () => {
15
+ rows = [['a'], ['b']];
16
+ const result = await db.getSchemas();
17
+ expect(result).toEqual(['a', 'b']);
18
+ });
19
+ });
20
+
21
+ describe('getTables', () => {
22
+ it('should return tables', async () => {
23
+ rows = [{ schemaName: 'schema', name: 'table' }];
24
+ const result = await db.getTables();
25
+ expect(result).toEqual(rows);
26
+ });
27
+ });
28
+
29
+ describe('getViews', () => {
30
+ it('should return views', async () => {
31
+ rows = [{ schemaName: 'schema', name: 'view' }];
32
+ const result = await db.getViews();
33
+ expect(result).toEqual(rows);
34
+ });
35
+ });
36
+
37
+ describe('getProcedures', () => {
38
+ it('should return procedures', async () => {
39
+ rows = [
40
+ {
41
+ schemaName: 'public',
42
+ name: 'name',
43
+ returnSet: true,
44
+ returnType: 'int4',
45
+ kind: 'f',
46
+ isTrigger: false,
47
+ types: ['int4', 'int4', 'int4'],
48
+ argTypes: [23, 23, 23],
49
+ argModes: ['i', 'i', 'o'],
50
+ argNames: ['a', 'b', 'c'],
51
+ },
52
+ ];
53
+ const result = await db.getProcedures();
54
+ expect(result).toEqual(rows);
55
+ });
56
+ });
57
+
58
+ describe('getColumns', () => {
59
+ it('should return columns', async () => {
60
+ rows = [
61
+ {
62
+ schemaName: 'public',
63
+ tableName: 'table',
64
+ name: 'name',
65
+ type: 'int4',
66
+ default: '123',
67
+ isNullable: false,
68
+ },
69
+ ];
70
+ const result = await db.getColumns();
71
+ expect(result).toEqual(rows);
72
+ });
73
+ });
74
+
75
+ describe('getIndexes', () => {
76
+ it('should return indexes', async () => {
77
+ rows = [
78
+ {
79
+ schemaName: 'public',
80
+ tableName: 'table',
81
+ columnNames: ['column'],
82
+ name: 'indexName',
83
+ isUnique: true,
84
+ isPrimary: true,
85
+ },
86
+ ];
87
+ const result = await db.getIndexes();
88
+ expect(result).toEqual(rows);
89
+ });
90
+ });
91
+
92
+ describe('getForeignKeys', () => {
93
+ it('should return foreignKeys', async () => {
94
+ rows = [
95
+ {
96
+ schemaName: 'public',
97
+ tableName: 'table',
98
+ foreignTableSchemaName: 'public',
99
+ foreignTableName: 'foreignTable',
100
+ name: 'name',
101
+ columnNames: ['column'],
102
+ foreignColumnNames: ['foreignColumn'],
103
+ },
104
+ ];
105
+ const result = await db.getForeignKeys();
106
+ expect(result).toEqual(rows);
107
+ });
108
+ });
109
+
110
+ describe('getConstraints', () => {
111
+ it('should return constraints', async () => {
112
+ rows = [
113
+ {
114
+ schemaName: 'public',
115
+ tableName: 'table',
116
+ name: 'name',
117
+ type: 'PRIMARY KEY',
118
+ columnNames: ['id'],
119
+ },
120
+ ];
121
+ const result = await db.getConstraints();
122
+ expect(result).toEqual(rows);
123
+ });
124
+ });
125
+
126
+ describe('getTriggers', () => {
127
+ it('should return triggers', async () => {
128
+ rows = [
129
+ {
130
+ schemaName: 'public',
131
+ tableName: 'table',
132
+ triggerSchema: 'public',
133
+ name: 'name',
134
+ events: ['UPDATE'],
135
+ activation: 'BEFORE',
136
+ condition: null,
137
+ definition: 'EXECUTE FUNCTION name()',
138
+ },
139
+ ];
140
+ const result = await db.getTriggers();
141
+ expect(result).toEqual(rows);
142
+ });
143
+ });
144
+
145
+ describe('getExtensions', () => {
146
+ it('should return extensions', async () => {
147
+ rows = [
148
+ {
149
+ schemaName: 'public',
150
+ name: 'pg_trgm',
151
+ version: '1.6',
152
+ },
153
+ ];
154
+ const result = await db.getExtensions();
155
+ expect(result).toEqual(rows);
156
+ });
157
+ });
158
+ });
@@ -0,0 +1,272 @@
1
+ import { Adapter } from 'pqb';
2
+
3
+ export namespace DbStructure {
4
+ export type Table = {
5
+ schemaName: string;
6
+ name: string;
7
+ };
8
+
9
+ export type View = {
10
+ schemaName: string;
11
+ name: string;
12
+ };
13
+
14
+ export type Procedure = {
15
+ schemaName: string;
16
+ name: string;
17
+ returnSet: boolean;
18
+ returnType: string;
19
+ kind: string;
20
+ isTrigger: boolean;
21
+ types: string[];
22
+ argTypes: string[];
23
+ argModes: ('i' | 'o')[];
24
+ argNames?: string[];
25
+ };
26
+
27
+ export type Column = {
28
+ schemaName: string;
29
+ tableName: string;
30
+ name: string;
31
+ type: string;
32
+ maxChars?: number;
33
+ numericPrecision?: number;
34
+ numericScale?: number;
35
+ dateTimePrecision?: number;
36
+ default?: string;
37
+ isNullable: boolean;
38
+ };
39
+
40
+ export type Index = {
41
+ schemaName: string;
42
+ tableName: string;
43
+ columnNames: string[];
44
+ name: string;
45
+ isUnique: boolean;
46
+ isPrimary: boolean;
47
+ };
48
+
49
+ export type ForeignKey = {
50
+ schemaName: string;
51
+ tableName: string;
52
+ foreignTableSchemaName: string;
53
+ foreignTableName: string;
54
+ name: string;
55
+ columnNames: string[];
56
+ foreignColumnNames: string[];
57
+ };
58
+
59
+ export type Constraint = {
60
+ schemaName: string;
61
+ tableName: string;
62
+ name: string;
63
+ type: 'CHECK' | 'FOREIGN KEY' | 'PRIMARY KEY' | 'UNIQUE';
64
+ columnNames: string[];
65
+ };
66
+
67
+ export type Trigger = {
68
+ schemaName: string;
69
+ tableName: string;
70
+ triggerSchema: string;
71
+ name: string;
72
+ events: string[];
73
+ activation: string;
74
+ condition?: string;
75
+ definition: string;
76
+ };
77
+
78
+ export type Extension = {
79
+ schemaName: string;
80
+ name: string;
81
+ version?: string;
82
+ };
83
+ }
84
+
85
+ const filterSchema = (table: string) =>
86
+ `${table} !~ '^pg_' AND ${table} != 'information_schema'`;
87
+
88
+ export class DbStructure {
89
+ constructor(private db: Adapter) {}
90
+
91
+ async getSchemas(): Promise<string[]> {
92
+ const { rows } = await this.db.arrays<[string]>(
93
+ `SELECT n.nspname "name"
94
+ FROM pg_catalog.pg_namespace n
95
+ WHERE ${filterSchema('n.nspname')}
96
+ ORDER BY "name"`,
97
+ );
98
+ return rows.flat();
99
+ }
100
+
101
+ async getTables() {
102
+ const { rows } = await this.db.query<DbStructure.Table>(
103
+ `SELECT
104
+ table_schema "schemaName",
105
+ table_name "name"
106
+ FROM information_schema.tables
107
+ WHERE table_type = 'BASE TABLE'
108
+ AND ${filterSchema('table_schema')}
109
+ ORDER BY table_name`,
110
+ );
111
+ return rows;
112
+ }
113
+
114
+ async getViews() {
115
+ const { rows } = await this.db.query<DbStructure.View[]>(
116
+ `SELECT
117
+ table_schema "schemaName",
118
+ table_name "name"
119
+ FROM information_schema.tables
120
+ WHERE table_type = 'VIEW'
121
+ AND ${filterSchema('table_schema')}
122
+ ORDER BY table_name`,
123
+ );
124
+ return rows;
125
+ }
126
+
127
+ async getProcedures() {
128
+ const { rows } = await this.db.query<DbStructure.Procedure[]>(
129
+ `SELECT
130
+ n.nspname AS "schemaName",
131
+ proname AS name,
132
+ proretset AS "returnSet",
133
+ (
134
+ SELECT typname FROM pg_type WHERE oid = prorettype
135
+ ) AS "returnType",
136
+ prokind AS "kind",
137
+ coalesce((
138
+ SELECT true FROM information_schema.triggers
139
+ WHERE n.nspname = trigger_schema AND trigger_name = proname
140
+ LIMIT 1
141
+ ), false) AS "isTrigger",
142
+ coalesce((
143
+ SELECT json_agg(pg_type.typname)
144
+ FROM unnest(coalesce(proallargtypes, proargtypes)) typeId
145
+ JOIN pg_type ON pg_type.oid = typeId
146
+ ), '[]') AS "types",
147
+ coalesce(to_json(proallargtypes::int[]), to_json(proargtypes::int[])) AS "argTypes",
148
+ coalesce(to_json(proargmodes), '[]') AS "argModes",
149
+ to_json(proargnames) AS "argNames"
150
+ FROM pg_proc p
151
+ JOIN pg_namespace n ON p.pronamespace = n.oid
152
+ WHERE ${filterSchema('n.nspname')}`,
153
+ );
154
+ return rows;
155
+ }
156
+
157
+ async getColumns() {
158
+ const { rows } = await this.db.query<DbStructure.Column>(
159
+ `SELECT table_schema "schemaName",
160
+ table_name "tableName",
161
+ column_name "name",
162
+ udt_name "type",
163
+ character_maximum_length AS "maxChars",
164
+ numeric_precision AS "numericPrecision",
165
+ numeric_scale AS "numericScale",
166
+ datetime_precision AS "dateTimePrecision",
167
+ column_default "default",
168
+ is_nullable::boolean "isNullable"
169
+ FROM information_schema.columns
170
+ WHERE ${filterSchema('table_schema')}
171
+ ORDER BY ordinal_position`,
172
+ );
173
+ return rows;
174
+ }
175
+
176
+ async getIndexes() {
177
+ const { rows } = await this.db.query<DbStructure.Index>(
178
+ `SELECT
179
+ nspname "schemaName",
180
+ t.relname "tableName",
181
+ json_agg(attname) "columnNames",
182
+ ic.relname "name",
183
+ indisunique "isUnique",
184
+ indisprimary "isPrimary"
185
+ FROM pg_index
186
+ JOIN pg_class t ON t.oid = indrelid
187
+ JOIN pg_namespace n ON n.oid = t.relnamespace
188
+ JOIN pg_attribute ON attrelid = t.oid AND attnum = any(indkey)
189
+ JOIN pg_class ic ON ic.oid = indexrelid
190
+ WHERE ${filterSchema('n.nspname')}
191
+ GROUP BY "schemaName", "tableName", "name", "isUnique", "isPrimary"
192
+ ORDER BY "name"`,
193
+ );
194
+ return rows;
195
+ }
196
+
197
+ async getForeignKeys() {
198
+ const { rows } = await this.db.query<DbStructure.ForeignKey>(
199
+ `SELECT tc.table_schema AS "schemaName",
200
+ tc.table_name AS "tableName",
201
+ ccu.table_schema AS "foreignTableSchemaName",
202
+ ccu.table_name AS "foreignTableName",
203
+ tc.constraint_name AS "name",
204
+ (
205
+ SELECT json_agg(kcu.column_name)
206
+ FROM information_schema.key_column_usage kcu
207
+ WHERE kcu.constraint_name = tc.constraint_name
208
+ AND kcu.table_schema = tc.table_schema
209
+ ) AS "columnNames",
210
+ json_agg(ccu.column_name) AS "foreignColumnNames"
211
+ FROM information_schema.table_constraints tc
212
+ JOIN information_schema.constraint_column_usage ccu
213
+ ON ccu.constraint_name = tc.constraint_name
214
+ AND ccu.table_schema = tc.table_schema
215
+ WHERE tc.constraint_type = 'FOREIGN KEY'
216
+ AND ${filterSchema('tc.table_schema')}
217
+ GROUP BY "schemaName", "tableName", "name", "foreignTableSchemaName", "foreignTableName"
218
+ ORDER BY "name"`,
219
+ );
220
+ return rows;
221
+ }
222
+
223
+ async getConstraints() {
224
+ const { rows } = await this.db.query<DbStructure.Constraint>(
225
+ `SELECT tc.table_schema AS "schemaName",
226
+ tc.table_name AS "tableName",
227
+ tc.constraint_name AS "name",
228
+ tc.constraint_type AS "type",
229
+ json_agg(ccu.column_name) "columnNames"
230
+ FROM information_schema.table_constraints tc
231
+ JOIN information_schema.constraint_column_usage ccu
232
+ ON ccu.constraint_name = tc.constraint_name
233
+ AND ccu.table_schema = tc.table_schema
234
+ WHERE tc.constraint_type != 'FOREIGN KEY'
235
+ AND ${filterSchema('tc.table_schema')}
236
+ GROUP BY "schemaName", "tableName", "name", "type"
237
+ ORDER BY "name"`,
238
+ );
239
+ return rows;
240
+ }
241
+
242
+ async getTriggers() {
243
+ const { rows } = await this.db.query<DbStructure.Trigger>(
244
+ `SELECT event_object_schema AS "schemaName",
245
+ event_object_table AS "tableName",
246
+ trigger_schema AS "triggerSchema",
247
+ trigger_name AS name,
248
+ json_agg(event_manipulation) AS events,
249
+ action_timing AS activation,
250
+ action_condition AS condition,
251
+ action_statement AS definition
252
+ FROM information_schema.triggers
253
+ WHERE ${filterSchema('event_object_schema')}
254
+ GROUP BY event_object_schema, event_object_table, trigger_schema, trigger_name, action_timing, action_condition, action_statement
255
+ ORDER BY trigger_name`,
256
+ );
257
+ return rows;
258
+ }
259
+
260
+ async getExtensions() {
261
+ const { rows } = await this.db.query<DbStructure.Extension>(
262
+ `SELECT
263
+ nspname AS "schemaName",
264
+ extname AS "name",
265
+ extversion AS version
266
+ FROM pg_extension
267
+ JOIN pg_catalog.pg_namespace n ON n.oid = extnamespace
268
+ AND ${filterSchema('n.nspname')}`,
269
+ );
270
+ return rows;
271
+ }
272
+ }
@@ -0,0 +1,15 @@
1
+ import { ColumnType, ColumnTypesBase } from 'pqb';
2
+
3
+ export const getColumnsByTypesMap = (types: ColumnTypesBase) => {
4
+ const map: Record<string, new () => ColumnType> = {};
5
+ for (const key in types) {
6
+ const type = types[key] as unknown as new () => ColumnType;
7
+ if (type instanceof ColumnType) {
8
+ map[type.dataType] = type;
9
+ if (type.typeAlias) {
10
+ map[type.typeAlias] = type;
11
+ }
12
+ }
13
+ }
14
+ return map;
15
+ };