@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.
@@ -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
+ }