|
| 1 | +# Schema Introspection - Connect to Existing Databases |
| 2 | + |
| 3 | +ObjectQL now supports **automatic schema introspection**, allowing you to connect to an existing database without defining any metadata. The system will automatically discover tables, columns, data types, and relationships. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- ✅ **Zero Metadata Required**: Connect to any existing SQL database instantly |
| 8 | +- ✅ **Automatic Type Mapping**: Database types are automatically mapped to ObjectQL field types |
| 9 | +- ✅ **Relationship Discovery**: Foreign keys are automatically detected and converted to lookup fields |
| 10 | +- ✅ **Selective Introspection**: Choose which tables to include or exclude |
| 11 | +- ✅ **Non-Destructive**: Introspection never modifies your existing database schema |
| 12 | + |
| 13 | +## Supported Databases |
| 14 | + |
| 15 | +- PostgreSQL |
| 16 | +- MySQL / MySQL2 |
| 17 | +- SQLite3 |
| 18 | + |
| 19 | +## Quick Start |
| 20 | + |
| 21 | +### 1. Connect to Existing Database |
| 22 | + |
| 23 | +```typescript |
| 24 | +import { ObjectQL } from '@objectql/core'; |
| 25 | +import { SqlDriver } from '@objectql/driver-sql'; |
| 26 | + |
| 27 | +// Create driver for your existing database |
| 28 | +const driver = new SqlDriver({ |
| 29 | + client: 'postgresql', |
| 30 | + connection: { |
| 31 | + host: 'localhost', |
| 32 | + database: 'my_existing_db', |
| 33 | + user: 'username', |
| 34 | + password: 'password' |
| 35 | + } |
| 36 | +}); |
| 37 | + |
| 38 | +// Initialize ObjectQL |
| 39 | +const app = new ObjectQL({ |
| 40 | + datasources: { default: driver } |
| 41 | +}); |
| 42 | +``` |
| 43 | + |
| 44 | +### 2. Introspect and Register Tables |
| 45 | + |
| 46 | +```typescript |
| 47 | +// Automatically discover all tables |
| 48 | +const objects = await app.introspectAndRegister(); |
| 49 | + |
| 50 | +console.log(`Discovered ${objects.length} tables`); |
| 51 | +``` |
| 52 | + |
| 53 | +### 3. Use Discovered Objects |
| 54 | + |
| 55 | +```typescript |
| 56 | +await app.init(); |
| 57 | + |
| 58 | +const ctx = app.createContext({ isSystem: true }); |
| 59 | + |
| 60 | +// Query existing data |
| 61 | +const users = await ctx.object('users').find({}); |
| 62 | + |
| 63 | +// Insert new records |
| 64 | +const newUser = await ctx.object('users').create({ |
| 65 | + name: 'Alice', |
| 66 | + email: 'alice@example.com' |
| 67 | +}); |
| 68 | +``` |
| 69 | + |
| 70 | +## Options |
| 71 | + |
| 72 | +### Selective Table Introspection |
| 73 | + |
| 74 | +```typescript |
| 75 | +// Only include specific tables |
| 76 | +await app.introspectAndRegister('default', { |
| 77 | + includeTables: ['users', 'orders', 'products'] |
| 78 | +}); |
| 79 | + |
| 80 | +// Or exclude certain tables |
| 81 | +await app.introspectAndRegister('default', { |
| 82 | + excludeTables: ['migrations', 'sessions', 'logs'] |
| 83 | +}); |
| 84 | +``` |
| 85 | + |
| 86 | +### System Columns |
| 87 | + |
| 88 | +By default, ObjectQL skips standard system columns (`id`, `created_at`, `updated_at`) during introspection since they're automatically handled by the framework. |
| 89 | + |
| 90 | +```typescript |
| 91 | +// Include system columns in field definitions |
| 92 | +await app.introspectAndRegister('default', { |
| 93 | + skipSystemColumns: false |
| 94 | +}); |
| 95 | +``` |
| 96 | + |
| 97 | +## Type Mapping |
| 98 | + |
| 99 | +Database types are automatically mapped to ObjectQL field types: |
| 100 | + |
| 101 | +| Database Type | ObjectQL Type | |
| 102 | +|--------------|---------------| |
| 103 | +| `VARCHAR`, `CHAR` | `text` | |
| 104 | +| `TEXT`, `LONGTEXT` | `textarea` | |
| 105 | +| `INTEGER`, `BIGINT`, `INT` | `number` | |
| 106 | +| `FLOAT`, `DOUBLE`, `DECIMAL` | `number` | |
| 107 | +| `BOOLEAN`, `BOOL` | `boolean` | |
| 108 | +| `DATE` | `date` | |
| 109 | +| `DATETIME`, `TIMESTAMP` | `datetime` | |
| 110 | +| `TIME` | `time` | |
| 111 | +| `JSON`, `JSONB` | `object` | |
| 112 | + |
| 113 | +## Relationship Detection |
| 114 | + |
| 115 | +Foreign key constraints are automatically detected and converted to `lookup` fields: |
| 116 | + |
| 117 | +**Database Schema:** |
| 118 | +```sql |
| 119 | +CREATE TABLE orders ( |
| 120 | + id VARCHAR PRIMARY KEY, |
| 121 | + customer_id VARCHAR NOT NULL, |
| 122 | + total DECIMAL(10, 2), |
| 123 | + FOREIGN KEY (customer_id) REFERENCES customers(id) |
| 124 | +); |
| 125 | +``` |
| 126 | + |
| 127 | +**Discovered ObjectQL Schema:** |
| 128 | +```typescript |
| 129 | +{ |
| 130 | + name: 'orders', |
| 131 | + fields: { |
| 132 | + customer_id: { |
| 133 | + type: 'lookup', |
| 134 | + reference_to: 'customers', |
| 135 | + required: true |
| 136 | + }, |
| 137 | + total: { |
| 138 | + type: 'number' |
| 139 | + } |
| 140 | + } |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +## Advanced Usage |
| 145 | + |
| 146 | +### Inspect Schema Without Registering |
| 147 | + |
| 148 | +```typescript |
| 149 | +const driver = app.datasource('default'); |
| 150 | +const schema = await driver.introspectSchema(); |
| 151 | + |
| 152 | +// schema.tables contains all discovered table metadata |
| 153 | +console.log(schema.tables['users'].columns); |
| 154 | +console.log(schema.tables['users'].foreignKeys); |
| 155 | +``` |
| 156 | + |
| 157 | +### Convert Schema Manually |
| 158 | + |
| 159 | +```typescript |
| 160 | +import { convertIntrospectedSchemaToObjects } from '@objectql/core'; |
| 161 | + |
| 162 | +const schema = await driver.introspectSchema(); |
| 163 | +const objects = convertIntrospectedSchemaToObjects(schema, { |
| 164 | + excludeTables: ['temp_tables'] |
| 165 | +}); |
| 166 | + |
| 167 | +// Register manually |
| 168 | +objects.forEach(obj => app.registerObject(obj)); |
| 169 | +``` |
| 170 | + |
| 171 | +## Use Cases |
| 172 | + |
| 173 | +### 1. Legacy Database Integration |
| 174 | + |
| 175 | +Connect ObjectQL to an existing legacy database without rewriting the schema: |
| 176 | + |
| 177 | +```typescript |
| 178 | +const app = new ObjectQL({ datasources: { legacy: legacyDriver } }); |
| 179 | +await app.introspectAndRegister('legacy'); |
| 180 | +await app.init(); |
| 181 | + |
| 182 | +// Now use ObjectQL's modern API with your legacy database |
| 183 | +const records = await ctx.object('old_table_name').find({}); |
| 184 | +``` |
| 185 | + |
| 186 | +### 2. Database Migration |
| 187 | + |
| 188 | +Introspect an existing database to generate ObjectQL metadata for version control: |
| 189 | + |
| 190 | +```typescript |
| 191 | +const schema = await driver.introspectSchema(); |
| 192 | +const objects = convertIntrospectedSchemaToObjects(schema); |
| 193 | + |
| 194 | +// Export to YAML files for your project |
| 195 | +objects.forEach(obj => { |
| 196 | + fs.writeFileSync( |
| 197 | + `src/objects/${obj.name}.object.yml`, |
| 198 | + yaml.dump(obj) |
| 199 | + ); |
| 200 | +}); |
| 201 | +``` |
| 202 | + |
| 203 | +### 3. Multi-Database Applications |
| 204 | + |
| 205 | +Connect to multiple existing databases simultaneously: |
| 206 | + |
| 207 | +```typescript |
| 208 | +const app = new ObjectQL({ |
| 209 | + datasources: { |
| 210 | + main: mainDbDriver, |
| 211 | + analytics: analyticsDbDriver, |
| 212 | + archive: archiveDbDriver |
| 213 | + } |
| 214 | +}); |
| 215 | + |
| 216 | +await app.introspectAndRegister('main'); |
| 217 | +await app.introspectAndRegister('analytics', { |
| 218 | + excludeTables: ['raw_logs'] |
| 219 | +}); |
| 220 | +await app.introspectAndRegister('archive', { |
| 221 | + includeTables: ['historical_orders'] |
| 222 | +}); |
| 223 | +``` |
| 224 | + |
| 225 | +## Limitations |
| 226 | + |
| 227 | +1. **SQLite Foreign Keys**: SQLite requires `PRAGMA foreign_keys = ON` to properly detect foreign key constraints |
| 228 | +2. **Complex Types**: Some database-specific types may be mapped to generic ObjectQL types |
| 229 | +3. **Computed Columns**: Virtual/computed columns are introspected as regular fields |
| 230 | +4. **Indexes**: While unique constraints are detected, regular indexes are not yet included in introspection |
| 231 | + |
| 232 | +## API Reference |
| 233 | + |
| 234 | +### `app.introspectAndRegister(datasourceName?, options?)` |
| 235 | + |
| 236 | +Introspect a database and automatically register discovered tables as ObjectQL objects. |
| 237 | + |
| 238 | +**Parameters:** |
| 239 | +- `datasourceName` (string, optional): Name of the datasource (default: `'default'`) |
| 240 | +- `options` (object, optional): |
| 241 | + - `includeTables` (string[]): Only include these tables |
| 242 | + - `excludeTables` (string[]): Exclude these tables |
| 243 | + - `skipSystemColumns` (boolean): Skip `id`, `created_at`, `updated_at` (default: `true`) |
| 244 | + |
| 245 | +**Returns:** `Promise<ObjectConfig[]>` - Array of registered object configurations |
| 246 | + |
| 247 | +### `driver.introspectSchema()` |
| 248 | + |
| 249 | +Low-level method to introspect the database schema. |
| 250 | + |
| 251 | +**Returns:** `Promise<IntrospectedSchema>` - Complete schema metadata |
| 252 | + |
| 253 | +### `convertIntrospectedSchemaToObjects(schema, options?)` |
| 254 | + |
| 255 | +Convert introspected schema to ObjectQL object configurations. |
| 256 | + |
| 257 | +**Parameters:** |
| 258 | +- `schema` (IntrospectedSchema): Schema from `driver.introspectSchema()` |
| 259 | +- `options` (object, optional): Same as `introspectAndRegister` |
| 260 | + |
| 261 | +**Returns:** `ObjectConfig[]` - Array of object configurations |
| 262 | + |
| 263 | +## Examples |
| 264 | + |
| 265 | +See [examples/connect-existing-database.ts](../examples/connect-existing-database.ts) for a complete working example. |
| 266 | + |
| 267 | +## Chinese Documentation (中文文档) |
| 268 | + |
| 269 | +### 快速开始 |
| 270 | + |
| 271 | +```typescript |
| 272 | +import { ObjectQL } from '@objectql/core'; |
| 273 | +import { SqlDriver } from '@objectql/driver-sql'; |
| 274 | + |
| 275 | +// 1. 连接到现有数据库 |
| 276 | +const driver = new SqlDriver({ |
| 277 | + client: 'postgresql', |
| 278 | + connection: { |
| 279 | + host: 'localhost', |
| 280 | + database: '现有数据库', |
| 281 | + user: '用户名', |
| 282 | + password: '密码' |
| 283 | + } |
| 284 | +}); |
| 285 | + |
| 286 | +const app = new ObjectQL({ |
| 287 | + datasources: { default: driver } |
| 288 | +}); |
| 289 | + |
| 290 | +// 2. 自动发现数据库表结构 |
| 291 | +const objects = await app.introspectAndRegister('default', { |
| 292 | + excludeTables: ['migrations'] // 排除不需要的表 |
| 293 | +}); |
| 294 | + |
| 295 | +console.log(`发现了 ${objects.length} 个表`); |
| 296 | + |
| 297 | +// 3. 初始化并使用 |
| 298 | +await app.init(); |
| 299 | +const ctx = app.createContext({ isSystem: true }); |
| 300 | + |
| 301 | +// 查询现有数据 |
| 302 | +const users = await ctx.object('users').find({}); |
| 303 | + |
| 304 | +// 创建新记录 |
| 305 | +const newUser = await ctx.object('users').create({ |
| 306 | + name: '张三', |
| 307 | + email: 'zhangsan@example.com' |
| 308 | +}); |
| 309 | +``` |
| 310 | + |
| 311 | +**主要特性:** |
| 312 | +- ✅ 无需定义任何元数据即可连接现有数据库 |
| 313 | +- ✅ 自动识别表、字段、数据类型和外键关系 |
| 314 | +- ✅ 支持 PostgreSQL、MySQL、SQLite |
| 315 | +- ✅ 完全非破坏性操作,不修改现有数据库结构 |
0 commit comments