@dbstudio/cli 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1372 -0
- package/package.json +36 -0
- package/src/agents/index.ts +458 -0
- package/src/commands/connect.ts +418 -0
- package/src/commands/disconnect.ts +37 -0
- package/src/commands/status.ts +54 -0
- package/src/drivers/index.ts +58 -0
- package/src/drivers/libsql.ts +189 -0
- package/src/drivers/mysql.ts +199 -0
- package/src/drivers/postgres.ts +224 -0
- package/src/drivers/sqlite.ts +206 -0
- package/src/index.ts +28 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ColumnInfo,
|
|
3
|
+
DatabaseConfig,
|
|
4
|
+
QueryResult,
|
|
5
|
+
TableInfo,
|
|
6
|
+
} from "@dbstudio/types";
|
|
7
|
+
import { type Client, createClient } from "@libsql/client";
|
|
8
|
+
import type { DatabaseDriver } from "./index";
|
|
9
|
+
|
|
10
|
+
export class LibSQLDriver implements DatabaseDriver {
|
|
11
|
+
private client: Client | null = null;
|
|
12
|
+
private config: DatabaseConfig;
|
|
13
|
+
|
|
14
|
+
constructor(config: DatabaseConfig) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async connect(): Promise<void> {
|
|
19
|
+
if (!this.config.url) {
|
|
20
|
+
throw new Error("libSQL requires a connection URL");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.client = createClient({
|
|
24
|
+
url: this.config.url,
|
|
25
|
+
authToken: this.config.authToken,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Test connection
|
|
29
|
+
await this.client.execute("SELECT 1");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async disconnect(): Promise<void> {
|
|
33
|
+
if (this.client) {
|
|
34
|
+
this.client.close();
|
|
35
|
+
this.client = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async query(sql: string, params?: unknown[]): Promise<QueryResult> {
|
|
40
|
+
if (!this.client) throw new Error("Not connected");
|
|
41
|
+
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
const result = await this.client.execute({
|
|
44
|
+
sql,
|
|
45
|
+
args: (params as any[]) || [],
|
|
46
|
+
});
|
|
47
|
+
const executionTimeMs = Date.now() - start;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
columns: result.columns,
|
|
51
|
+
rows: result.rows.map((row) => {
|
|
52
|
+
const obj: Record<string, unknown> = {};
|
|
53
|
+
result.columns.forEach((col, i) => {
|
|
54
|
+
obj[col] = row[i];
|
|
55
|
+
});
|
|
56
|
+
return obj;
|
|
57
|
+
}),
|
|
58
|
+
rowCount: result.rows.length,
|
|
59
|
+
affectedRows: result.rowsAffected,
|
|
60
|
+
executionTimeMs,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getTables(): Promise<TableInfo[]> {
|
|
65
|
+
if (!this.client) throw new Error("Not connected");
|
|
66
|
+
|
|
67
|
+
const result = await this.client.execute(
|
|
68
|
+
`SELECT name FROM sqlite_master
|
|
69
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
70
|
+
ORDER BY name`,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const tables: TableInfo[] = [];
|
|
74
|
+
for (const row of result.rows) {
|
|
75
|
+
const tableName = row[0] as string;
|
|
76
|
+
try {
|
|
77
|
+
const tableInfo = await this.getTableSchema(tableName);
|
|
78
|
+
tables.push(tableInfo);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(`Failed to get schema for table ${tableName}:`, error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return tables;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getTableSchema(table: string): Promise<TableInfo> {
|
|
88
|
+
if (!this.client) throw new Error("Not connected");
|
|
89
|
+
|
|
90
|
+
// Get columns
|
|
91
|
+
const columnsResult = await this.client.execute(
|
|
92
|
+
`PRAGMA table_info("${table}")`,
|
|
93
|
+
);
|
|
94
|
+
const columns: ColumnInfo[] = columnsResult.rows.map((row) => ({
|
|
95
|
+
name: row[1] as string,
|
|
96
|
+
type: row[2] as string,
|
|
97
|
+
nullable: (row[3] as number) === 0,
|
|
98
|
+
defaultValue: (row[4] as string) || undefined,
|
|
99
|
+
isPrimaryKey: (row[5] as number) === 1,
|
|
100
|
+
isForeignKey: false, // Will be updated by relations
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
// Get row count
|
|
104
|
+
const countResult = await this.client.execute(
|
|
105
|
+
`SELECT COUNT(*) as count FROM "${table}"`,
|
|
106
|
+
);
|
|
107
|
+
const rowCount = countResult.rows[0][0] as number;
|
|
108
|
+
|
|
109
|
+
// Get indexes
|
|
110
|
+
const indexListResult = await this.client.execute(
|
|
111
|
+
`PRAGMA index_list("${table}")`,
|
|
112
|
+
);
|
|
113
|
+
const indexes = await Promise.all(
|
|
114
|
+
indexListResult.rows.map(async (row) => {
|
|
115
|
+
const indexName = row[1] as string;
|
|
116
|
+
const isUnique = (row[2] as number) === 1;
|
|
117
|
+
const indexInfoResult = await this.client!.execute(
|
|
118
|
+
`PRAGMA index_info("${indexName}")`,
|
|
119
|
+
);
|
|
120
|
+
return {
|
|
121
|
+
name: indexName,
|
|
122
|
+
unique: isUnique,
|
|
123
|
+
columns: indexInfoResult.rows.map((r) => r[2] as string),
|
|
124
|
+
};
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Get foreign keys for relations
|
|
129
|
+
const fkListResult = await this.client.execute(
|
|
130
|
+
`PRAGMA foreign_key_list("${table}")`,
|
|
131
|
+
);
|
|
132
|
+
const relations = fkListResult.rows.map((row) => ({
|
|
133
|
+
name: `${table}_${row[3] as string}_fk`,
|
|
134
|
+
type: "belongsTo" as const,
|
|
135
|
+
fromTable: table,
|
|
136
|
+
fromColumn: row[3] as string,
|
|
137
|
+
toTable: row[2] as string,
|
|
138
|
+
toColumn: row[4] as string,
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
// Mark foreign key columns
|
|
142
|
+
for (const rel of relations) {
|
|
143
|
+
const col = columns.find((c) => c.name === rel.fromColumn);
|
|
144
|
+
if (col) col.isForeignKey = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
name: table,
|
|
149
|
+
schema: "main",
|
|
150
|
+
columns,
|
|
151
|
+
rowCount,
|
|
152
|
+
indexes,
|
|
153
|
+
relations,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async insertRow(
|
|
158
|
+
table: string,
|
|
159
|
+
data: Record<string, unknown>,
|
|
160
|
+
): Promise<QueryResult> {
|
|
161
|
+
if (!this.client) throw new Error("Not connected");
|
|
162
|
+
|
|
163
|
+
const columns = Object.keys(data);
|
|
164
|
+
const values = Object.values(data);
|
|
165
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
166
|
+
const sql = `INSERT INTO "${table}" (${columns.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`;
|
|
167
|
+
|
|
168
|
+
return this.query(sql, values);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async updateRow(
|
|
172
|
+
table: string,
|
|
173
|
+
data: Record<string, unknown>,
|
|
174
|
+
where: Record<string, unknown>,
|
|
175
|
+
): Promise<QueryResult> {
|
|
176
|
+
if (!this.client) throw new Error("Not connected");
|
|
177
|
+
|
|
178
|
+
const setClauses = Object.keys(data)
|
|
179
|
+
.map((c) => `"${c}" = ?`)
|
|
180
|
+
.join(", ");
|
|
181
|
+
const whereClauses = Object.keys(where)
|
|
182
|
+
.map((c) => `"${c}" = ?`)
|
|
183
|
+
.join(" AND ");
|
|
184
|
+
const values = [...Object.values(data), ...Object.values(where)];
|
|
185
|
+
const sql = `UPDATE "${table}" SET ${setClauses} WHERE ${whereClauses}`;
|
|
186
|
+
|
|
187
|
+
return this.query(sql, values);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ColumnInfo,
|
|
3
|
+
DatabaseConfig,
|
|
4
|
+
QueryResult,
|
|
5
|
+
TableInfo,
|
|
6
|
+
} from "@dbstudio/types";
|
|
7
|
+
import mysql from "mysql2/promise";
|
|
8
|
+
import type { DatabaseDriver } from "./index";
|
|
9
|
+
|
|
10
|
+
export class MySQLDriver implements DatabaseDriver {
|
|
11
|
+
private connection: mysql.Connection | null = null;
|
|
12
|
+
private config: DatabaseConfig;
|
|
13
|
+
|
|
14
|
+
constructor(config: DatabaseConfig) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async connect(): Promise<void> {
|
|
19
|
+
if (this.config.url) {
|
|
20
|
+
this.connection = await mysql.createConnection(this.config.url);
|
|
21
|
+
} else {
|
|
22
|
+
this.connection = await mysql.createConnection({
|
|
23
|
+
host: this.config.host,
|
|
24
|
+
port: this.config.port,
|
|
25
|
+
database: this.config.database,
|
|
26
|
+
user: this.config.username,
|
|
27
|
+
password: this.config.password,
|
|
28
|
+
ssl: this.config.ssl ? {} : undefined,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async disconnect(): Promise<void> {
|
|
34
|
+
if (this.connection) {
|
|
35
|
+
await this.connection.end();
|
|
36
|
+
this.connection = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async query(sql: string, params?: unknown[]): Promise<QueryResult> {
|
|
41
|
+
if (!this.connection) throw new Error("Not connected");
|
|
42
|
+
|
|
43
|
+
const start = Date.now();
|
|
44
|
+
const [rows, fields] = await this.connection.execute(sql, params);
|
|
45
|
+
const executionTimeMs = Date.now() - start;
|
|
46
|
+
|
|
47
|
+
const isResultSet = Array.isArray(rows);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
columns: fields ? (fields as mysql.FieldPacket[]).map((f) => f.name) : [],
|
|
51
|
+
rows: isResultSet ? (rows as Record<string, unknown>[]) : [],
|
|
52
|
+
rowCount: isResultSet ? rows.length : 0,
|
|
53
|
+
affectedRows: !isResultSet
|
|
54
|
+
? (rows as mysql.ResultSetHeader).affectedRows
|
|
55
|
+
: undefined,
|
|
56
|
+
executionTimeMs,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getTables(schema?: string): Promise<TableInfo[]> {
|
|
61
|
+
if (!this.connection) throw new Error("Not connected");
|
|
62
|
+
|
|
63
|
+
const db = schema || this.config.database;
|
|
64
|
+
const [rows] = await this.connection.execute(
|
|
65
|
+
`SELECT table_name
|
|
66
|
+
FROM information_schema.tables
|
|
67
|
+
WHERE table_schema = ? AND table_type = 'BASE TABLE'
|
|
68
|
+
ORDER BY table_name`,
|
|
69
|
+
[db],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const tables: TableInfo[] = [];
|
|
73
|
+
for (const row of rows as any[]) {
|
|
74
|
+
const tableInfo = await this.getTableSchema(
|
|
75
|
+
row.TABLE_NAME || row.table_name,
|
|
76
|
+
db,
|
|
77
|
+
);
|
|
78
|
+
tables.push(tableInfo);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return tables;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getTableSchema(table: string, schema?: string): Promise<TableInfo> {
|
|
85
|
+
if (!this.connection) throw new Error("Not connected");
|
|
86
|
+
|
|
87
|
+
const db = schema || this.config.database;
|
|
88
|
+
|
|
89
|
+
const [columnsRows] = await this.connection.execute(
|
|
90
|
+
`SELECT
|
|
91
|
+
COLUMN_NAME as column_name,
|
|
92
|
+
DATA_TYPE as data_type,
|
|
93
|
+
IS_NULLABLE as is_nullable,
|
|
94
|
+
COLUMN_DEFAULT as column_default,
|
|
95
|
+
COLUMN_KEY as column_key
|
|
96
|
+
FROM information_schema.columns
|
|
97
|
+
WHERE table_schema = ? AND table_name = ?
|
|
98
|
+
ORDER BY ordinal_position`,
|
|
99
|
+
[db, table],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const columns: ColumnInfo[] = (columnsRows as any[]).map((row) => ({
|
|
103
|
+
name: row.column_name,
|
|
104
|
+
type: row.data_type,
|
|
105
|
+
nullable: row.is_nullable === "YES",
|
|
106
|
+
defaultValue: row.column_default,
|
|
107
|
+
isPrimaryKey: row.column_key === "PRI",
|
|
108
|
+
isForeignKey: row.column_key === "MUL",
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
const [countRows] = await this.connection.execute(
|
|
112
|
+
`SELECT COUNT(*) as count FROM \`${db}\`.\`${table}\``,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Get indexes
|
|
116
|
+
const [indexesRows] = await this.connection.execute(
|
|
117
|
+
`SHOW INDEX FROM \`${table}\` FROM \`${db}\``,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const indexesMap = new Map<
|
|
121
|
+
string,
|
|
122
|
+
{ name: string; columns: string[]; unique: boolean }
|
|
123
|
+
>();
|
|
124
|
+
(indexesRows as any[]).forEach((row) => {
|
|
125
|
+
if (!indexesMap.has(row.Key_name)) {
|
|
126
|
+
indexesMap.set(row.Key_name, {
|
|
127
|
+
name: row.Key_name,
|
|
128
|
+
columns: [],
|
|
129
|
+
unique: row.Non_unique === 0,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
indexesMap.get(row.Key_name)!.columns.push(row.Column_name);
|
|
133
|
+
});
|
|
134
|
+
const indexes = Array.from(indexesMap.values());
|
|
135
|
+
|
|
136
|
+
// Get relations
|
|
137
|
+
const [relationsRows] = await this.connection.execute(
|
|
138
|
+
`SELECT
|
|
139
|
+
CONSTRAINT_NAME as constraint_name,
|
|
140
|
+
COLUMN_NAME as column_name,
|
|
141
|
+
REFERENCED_TABLE_NAME as referenced_table_name,
|
|
142
|
+
REFERENCED_COLUMN_NAME as referenced_column_name
|
|
143
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
144
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND REFERENCED_TABLE_NAME IS NOT NULL`,
|
|
145
|
+
[db, table],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const relations = (relationsRows as any[]).map((row) => ({
|
|
149
|
+
name: row.constraint_name,
|
|
150
|
+
type: "belongsTo" as const,
|
|
151
|
+
fromTable: table,
|
|
152
|
+
fromColumn: row.column_name,
|
|
153
|
+
toTable: row.referenced_table_name,
|
|
154
|
+
toColumn: row.referenced_column_name,
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
name: table,
|
|
159
|
+
schema: db!,
|
|
160
|
+
columns,
|
|
161
|
+
rowCount: Number.parseInt((countRows as any[])[0].count, 10),
|
|
162
|
+
indexes,
|
|
163
|
+
relations,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async insertRow(
|
|
168
|
+
table: string,
|
|
169
|
+
data: Record<string, unknown>,
|
|
170
|
+
): Promise<QueryResult> {
|
|
171
|
+
if (!this.connection) throw new Error("Not connected");
|
|
172
|
+
|
|
173
|
+
const columns = Object.keys(data);
|
|
174
|
+
const values = Object.values(data);
|
|
175
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
176
|
+
const sql = `INSERT INTO \`${table}\` (${columns.map((c) => `\`${c}\``).join(", ")}) VALUES (${placeholders})`;
|
|
177
|
+
|
|
178
|
+
return this.query(sql, values);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async updateRow(
|
|
182
|
+
table: string,
|
|
183
|
+
data: Record<string, unknown>,
|
|
184
|
+
where: Record<string, unknown>,
|
|
185
|
+
): Promise<QueryResult> {
|
|
186
|
+
if (!this.connection) throw new Error("Not connected");
|
|
187
|
+
|
|
188
|
+
const setClauses = Object.keys(data)
|
|
189
|
+
.map((c) => `\`${c}\` = ?`)
|
|
190
|
+
.join(", ");
|
|
191
|
+
const whereClauses = Object.keys(where)
|
|
192
|
+
.map((c) => `\`${c}\` = ?`)
|
|
193
|
+
.join(" AND ");
|
|
194
|
+
const values = [...Object.values(data), ...Object.values(where)];
|
|
195
|
+
const sql = `UPDATE \`${table}\` SET ${setClauses} WHERE ${whereClauses}`;
|
|
196
|
+
|
|
197
|
+
return this.query(sql, values);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ColumnInfo,
|
|
3
|
+
DatabaseConfig,
|
|
4
|
+
QueryResult,
|
|
5
|
+
TableInfo,
|
|
6
|
+
} from "@dbstudio/types";
|
|
7
|
+
import pg from "pg";
|
|
8
|
+
import type { DatabaseDriver } from "./index";
|
|
9
|
+
|
|
10
|
+
const { Pool } = pg;
|
|
11
|
+
|
|
12
|
+
export class PostgresDriver implements DatabaseDriver {
|
|
13
|
+
private pool: pg.Pool | null = null;
|
|
14
|
+
private config: DatabaseConfig;
|
|
15
|
+
|
|
16
|
+
constructor(config: DatabaseConfig) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async connect(): Promise<void> {
|
|
21
|
+
if (this.config.url) {
|
|
22
|
+
this.pool = new Pool({
|
|
23
|
+
connectionString: this.config.url,
|
|
24
|
+
ssl: this.config.ssl ? { rejectUnauthorized: false } : undefined,
|
|
25
|
+
});
|
|
26
|
+
} else {
|
|
27
|
+
this.pool = new Pool({
|
|
28
|
+
host: this.config.host,
|
|
29
|
+
port: this.config.port,
|
|
30
|
+
database: this.config.database,
|
|
31
|
+
user: this.config.username,
|
|
32
|
+
password: this.config.password || "",
|
|
33
|
+
ssl: this.config.ssl ? { rejectUnauthorized: false } : undefined,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Test connection
|
|
38
|
+
const client = await this.pool.connect();
|
|
39
|
+
client.release();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async disconnect(): Promise<void> {
|
|
43
|
+
const pool = this.pool;
|
|
44
|
+
this.pool = null;
|
|
45
|
+
if (pool) {
|
|
46
|
+
await pool.end();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async query(sql: string, params?: unknown[]): Promise<QueryResult> {
|
|
51
|
+
if (!this.pool) throw new Error("Not connected");
|
|
52
|
+
|
|
53
|
+
const start = Date.now();
|
|
54
|
+
const result = await this.pool.query(sql, params);
|
|
55
|
+
const executionTimeMs = Date.now() - start;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
columns: result.fields.map((f) => f.name),
|
|
59
|
+
rows: result.rows,
|
|
60
|
+
rowCount: result.rows.length,
|
|
61
|
+
affectedRows: result.rowCount ?? undefined,
|
|
62
|
+
executionTimeMs,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async getTables(schema = "public"): Promise<TableInfo[]> {
|
|
67
|
+
if (!this.pool) throw new Error("Not connected");
|
|
68
|
+
|
|
69
|
+
const result = await this.pool.query(
|
|
70
|
+
`SELECT table_name
|
|
71
|
+
FROM information_schema.tables
|
|
72
|
+
WHERE table_schema = $1 AND table_type = 'BASE TABLE'
|
|
73
|
+
ORDER BY table_name`,
|
|
74
|
+
[schema],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const tables: TableInfo[] = [];
|
|
78
|
+
for (const row of result.rows) {
|
|
79
|
+
const tableInfo = await this.getTableSchema(row.table_name, schema);
|
|
80
|
+
tables.push(tableInfo);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return tables;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getTableSchema(table: string, schema = "public"): Promise<TableInfo> {
|
|
87
|
+
if (!this.pool) throw new Error("Not connected");
|
|
88
|
+
|
|
89
|
+
// Get columns
|
|
90
|
+
const columnsResult = await this.pool.query(
|
|
91
|
+
`SELECT
|
|
92
|
+
column_name,
|
|
93
|
+
data_type,
|
|
94
|
+
is_nullable,
|
|
95
|
+
column_default
|
|
96
|
+
FROM information_schema.columns
|
|
97
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
98
|
+
ORDER BY ordinal_position`,
|
|
99
|
+
[schema, table],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Get constraints for this table's columns
|
|
103
|
+
const constraintsResult = await this.pool.query(
|
|
104
|
+
`SELECT
|
|
105
|
+
ccu.column_name,
|
|
106
|
+
tc.constraint_type
|
|
107
|
+
FROM information_schema.table_constraints tc
|
|
108
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
109
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
110
|
+
AND tc.table_schema = ccu.table_schema
|
|
111
|
+
WHERE tc.table_schema = $1 AND tc.table_name = $2`,
|
|
112
|
+
[schema, table],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const columnConstraints = new Map<string, string[]>();
|
|
116
|
+
for (const row of constraintsResult.rows) {
|
|
117
|
+
if (!columnConstraints.has(row.column_name)) {
|
|
118
|
+
columnConstraints.set(row.column_name, []);
|
|
119
|
+
}
|
|
120
|
+
columnConstraints.get(row.column_name)?.push(row.constraint_type);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const columns: ColumnInfo[] = columnsResult.rows.map((row) => {
|
|
124
|
+
const constraints = columnConstraints.get(row.column_name) || [];
|
|
125
|
+
return {
|
|
126
|
+
name: row.column_name,
|
|
127
|
+
type: row.data_type,
|
|
128
|
+
nullable: row.is_nullable === "YES",
|
|
129
|
+
defaultValue: row.column_default,
|
|
130
|
+
isPrimaryKey: constraints.includes("PRIMARY KEY"),
|
|
131
|
+
isForeignKey: constraints.includes("FOREIGN KEY"),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Get indexes
|
|
136
|
+
const indexesResult = await this.pool.query(
|
|
137
|
+
`SELECT indexname, indexdef
|
|
138
|
+
FROM pg_indexes
|
|
139
|
+
WHERE schemaname = $1 AND tablename = $2`,
|
|
140
|
+
[schema, table],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const indexes = indexesResult.rows.map((row) => ({
|
|
144
|
+
name: row.indexname,
|
|
145
|
+
columns: [], // Parsing columns from indexdef is complex, leaving empty for now or need regex
|
|
146
|
+
unique: row.indexdef.includes("UNIQUE"),
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
// Get relations
|
|
150
|
+
const relationsResult = await this.pool.query(
|
|
151
|
+
`SELECT
|
|
152
|
+
tc.constraint_name,
|
|
153
|
+
tc.constraint_type,
|
|
154
|
+
kcu.column_name,
|
|
155
|
+
ccu.table_name AS foreign_table_name,
|
|
156
|
+
ccu.column_name AS foreign_column_name
|
|
157
|
+
FROM information_schema.table_constraints AS tc
|
|
158
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
159
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
160
|
+
AND tc.table_schema = kcu.table_schema
|
|
161
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
162
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
163
|
+
AND ccu.table_schema = tc.table_schema
|
|
164
|
+
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1 AND tc.table_name = $2`,
|
|
165
|
+
[schema, table],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const relations = relationsResult.rows.map((row) => ({
|
|
169
|
+
name: row.constraint_name,
|
|
170
|
+
type: "belongsTo" as const,
|
|
171
|
+
fromTable: table,
|
|
172
|
+
fromColumn: row.column_name,
|
|
173
|
+
toTable: row.foreign_table_name,
|
|
174
|
+
toColumn: row.foreign_column_name,
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
// Get row count
|
|
178
|
+
const countResult = await this.pool.query(
|
|
179
|
+
`SELECT COUNT(*) as count FROM "${schema}"."${table}"`,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
name: table,
|
|
184
|
+
schema,
|
|
185
|
+
columns,
|
|
186
|
+
rowCount: Number.parseInt(countResult.rows[0].count, 10),
|
|
187
|
+
indexes,
|
|
188
|
+
relations,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async insertRow(
|
|
193
|
+
table: string,
|
|
194
|
+
data: Record<string, unknown>,
|
|
195
|
+
): Promise<QueryResult> {
|
|
196
|
+
if (!this.pool) throw new Error("Not connected");
|
|
197
|
+
|
|
198
|
+
const columns = Object.keys(data);
|
|
199
|
+
const values = Object.values(data);
|
|
200
|
+
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
|
201
|
+
const sql = `INSERT INTO "${table}" (${columns.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`;
|
|
202
|
+
|
|
203
|
+
return this.query(sql, values);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async updateRow(
|
|
207
|
+
table: string,
|
|
208
|
+
data: Record<string, unknown>,
|
|
209
|
+
where: Record<string, unknown>,
|
|
210
|
+
): Promise<QueryResult> {
|
|
211
|
+
if (!this.pool) throw new Error("Not connected");
|
|
212
|
+
|
|
213
|
+
const setClauses = Object.keys(data)
|
|
214
|
+
.map((c, i) => `"${c}" = $${i + 1}`)
|
|
215
|
+
.join(", ");
|
|
216
|
+
const whereClauses = Object.keys(where)
|
|
217
|
+
.map((c, i) => `"${c}" = $${Object.keys(data).length + i + 1}`)
|
|
218
|
+
.join(" AND ");
|
|
219
|
+
const values = [...Object.values(data), ...Object.values(where)];
|
|
220
|
+
const sql = `UPDATE "${table}" SET ${setClauses} WHERE ${whereClauses}`;
|
|
221
|
+
|
|
222
|
+
return this.query(sql, values);
|
|
223
|
+
}
|
|
224
|
+
}
|