Chapter 3: Schema Design and Code Generation
Overview
Schema design is the foundation of every Vektagraf application. A well-designed schema not only defines your data structure but also drives code generation, security policies, and performance optimizations. This chapter provides comprehensive guidance on creating robust, scalable schemas and leveraging Vektagraf's powerful code generation capabilities.
You'll learn to design schemas that evolve gracefully, generate type-safe Dart code, and implement advanced patterns for complex applications. By mastering schema-driven development, you'll build applications that are maintainable, secure, and performant from day one.
Learning Objectives
By the end of this chapter, you will be able to:
- Design comprehensive Vektagraf schemas in JSON with proper validation and constraints
- Understand and implement all Vektagraf field types and relationships
- Generate type-safe Dart code using both CLI and programmatic approaches
- Implement schema evolution and migration strategies
- Apply advanced schema patterns for complex applications
- Optimize schemas for performance and maintainability
Prerequisites
- Completion of Chapter 2 (Getting Started and Basic Usage)
- Understanding of JSON format and structure
- Basic knowledge of data modeling concepts
- Familiarity with Dart type system
JSON Schema Format
Vektagraf uses a structured JSON format to define your application's data model. This schema serves as the single source of truth for your entire data layer.
Basic Schema Structure
Every Vektagraf schema follows this basic structure:
{
"$schema": "vektagraf://schema/v1",
"version": "1.0.0",
"metadata": {
"name": "My Application Schema",
"description": "Schema for my awesome application",
"author": "Your Name",
"created": "2024-01-01T00:00:00Z"
},
"models": {
"model_name": {
"field_name": "field_type",
"another_field": {
"type": "complex_type",
"validation": { /* validation rules */ },
"metadata": { /* field metadata */ }
}
}
},
"security": {
/* security configuration */
}
}
Schema Metadata
The schema metadata section provides important information about your schema:
{
"$schema": "vektagraf://schema/v1",
"version": "2.1.0",
"metadata": {
"name": "E-commerce Platform Schema",
"description": "Complete schema for e-commerce platform with products, orders, and users",
"author": "Development Team",
"created": "2024-01-01T00:00:00Z",
"updated": "2024-03-15T10:30:00Z",
"tags": ["e-commerce", "retail", "platform"],
"compatibility": {
"minimum_vektagraf_version": "0.1.0",
"dart_sdk": ">=3.0.0"
}
}
}
Model Definitions
Models are the core of your schema. Each model represents an object type in your application:
{
"models": {
"user": {
"$meta": {
"description": "Application user with profile and preferences",
"timestamps": true,
"soft_delete": true,
"indexes": ["email", "username"]
},
"id": "id",
"username": {
"type": "string",
"validation": {
"minLength": 3,
"maxLength": 50,
"pattern": "^[a-zA-Z0-9_]+$"
},
"unique": true
},
"email": {
"type": "string",
"validation": {
"format": "email"
},
"unique": true
},
"profile": "@user_profile",
"orders": "[@order]",
"created_at": "datetime",
"updated_at": "datetime"
}
}
}
Field Types and Validation
Vektagraf supports a rich set of field types with comprehensive validation options.
Basic Types
Primitive Types
{
"user": {
"id": "id", // Unique identifier
"name": "string", // Text data
"age": "int32", // 32-bit integer
"balance": "float64", // 64-bit floating point
"is_active": "bool", // Boolean value
"birth_date": "datetime", // Date and time
"avatar": "bytes", // Binary data
"metadata": "dynamic" // Any JSON value
}
}
Integer Types
{
"analytics": {
"small_count": "int8", // -128 to 127
"medium_count": "int16", // -32,768 to 32,767
"large_count": "int32", // -2^31 to 2^31-1
"huge_count": "int64", // -2^63 to 2^63-1
"positive_count": "uint32", // 0 to 2^32-1
"big_positive": "uint64" // 0 to 2^64-1
}
}
Floating Point Types
{
"measurements": {
"temperature": "float32", // Single precision
"precise_value": "float64", // Double precision
"coordinates": {
"type": "vector",
"dimensions": 2,
"element_type": "float32" // Vector element type
}
}
}
Collection Types
Lists (Ordered Collections)
{
"article": {
"tags": "[string]", // List of strings
"ratings": "[int32]", // List of integers
"comments": "[@comment]", // List of comment objects
"attachments": "[bytes]" // List of binary data
}
}
Sets (Unique Collections)
{
"user": {
"skills": "{string}", // Set of unique strings
"badges": "{@badge}", // Set of unique badge objects
"permissions": "{string}" // Set of unique permissions
}
}
Maps (Key-Value Collections)
{
"configuration": {
"settings": "<string, string>", // String to string map
"counters": "<string, int32>", // String to integer map
"metadata": "<string, dynamic>", // String to any value map
"relationships": "<string, @user>" // String to user object map
}
}
Vector Types
Vector fields enable similarity search and machine learning operations:
{
"document": {
"content_embedding": {
"type": "vector",
"dimensions": 1536,
"distance_function": "cosine",
"element_type": "float32",
"metadata": {
"model": "text-embedding-ada-002",
"description": "OpenAI embedding for semantic search"
}
},
"image_features": {
"type": "vector",
"dimensions": 2048,
"distance_function": "euclidean",
"element_type": "float32",
"sparse": true,
"metadata": {
"model": "ResNet-50",
"layer": "avg_pool"
}
}
}
}
Distance Functions
{
"similarity_config": {
"cosine_vector": {
"type": "vector",
"dimensions": 384,
"distance_function": "cosine" // Best for normalized vectors
},
"euclidean_vector": {
"type": "vector",
"dimensions": 128,
"distance_function": "euclidean" // Best for spatial data
},
"inner_product_vector": {
"type": "vector",
"dimensions": 512,
"distance_function": "inner_product" // Best for learned embeddings
},
"manhattan_vector": {
"type": "vector",
"dimensions": 256,
"distance_function": "manhattan" // Best for sparse vectors
}
}
}
Relationship Types
Relationships define how objects connect to each other:
One-to-One Relationships
{
"user": {
"profile": "@user_profile" // Each user has one profile
},
"user_profile": {
"user": "@user" // Each profile belongs to one user
}
}
One-to-Many Relationships
{
"author": {
"books": "[@book]" // Author has many books
},
"book": {
"author": "@author" // Book has one author
}
}
Many-to-Many Relationships
{
"student": {
"courses": "{@course}" // Student enrolled in many courses
},
"course": {
"students": "{@student}" // Course has many students
}
}
Bidirectional Relationships
{
"user": {
"friends": {
"type": "{@user}",
"bidirectional": true,
"inverse_field": "friends"
}
}
}
Validation Rules
Comprehensive validation ensures data integrity:
String Validation
{
"user": {
"username": {
"type": "string",
"validation": {
"minLength": 3,
"maxLength": 50,
"pattern": "^[a-zA-Z0-9_]+$",
"format": "username"
}
},
"email": {
"type": "string",
"validation": {
"format": "email",
"maxLength": 255
}
},
"phone": {
"type": "string",
"validation": {
"pattern": "^\\+?[1-9]\\d{1,14}$",
"format": "phone"
}
},
"website": {
"type": "string",
"validation": {
"format": "uri",
"schemes": ["http", "https"]
}
}
}
}
Numeric Validation
{
"product": {
"price": {
"type": "float64",
"validation": {
"minimum": 0.01,
"maximum": 999999.99,
"multipleOf": 0.01
}
},
"quantity": {
"type": "int32",
"validation": {
"minimum": 0,
"maximum": 10000
}
},
"rating": {
"type": "float32",
"validation": {
"minimum": 1.0,
"maximum": 5.0,
"exclusiveMinimum": false,
"exclusiveMaximum": false
}
}
}
}
Collection Validation
{
"article": {
"tags": {
"type": "[string]",
"validation": {
"minItems": 1,
"maxItems": 10,
"uniqueItems": true,
"items": {
"minLength": 2,
"maxLength": 30,
"pattern": "^[a-z0-9-]+$"
}
}
},
"categories": {
"type": "{string}",
"validation": {
"minItems": 1,
"maxItems": 5,
"enum": ["tech", "science", "business", "lifestyle", "sports"]
}
}
}
}
Custom Validation
{
"event": {
"start_date": "datetime",
"end_date": {
"type": "datetime",
"validation": {
"custom": "end_date > start_date",
"message": "End date must be after start date"
}
},
"capacity": {
"type": "int32",
"validation": {
"custom": "capacity > 0 && capacity <= 10000",
"message": "Capacity must be between 1 and 10,000"
}
}
}
}
Code Generation Process
Vektagraf's code generator transforms your JSON schema into type-safe Dart code with full IDE support.
CLI Code Generation
The command-line interface provides the simplest way to generate code:
Basic Generation
# Generate code from schema
dart run vektagraf:generate \
--schema schema/app.json \
--output lib/generated/ \
--package-name my_app
# Generate with specific options
dart run vektagraf:generate \
--schema schema/app.json \
--output lib/generated/ \
--package-name my_app \
--generate-toString \
--generate-equality \
--generate-copy-with \
--generate-serialization
Advanced CLI Options
# Generate with custom configuration
dart run vektagraf:generate \
--schema schema/app.json \
--output lib/generated/ \
--package-name my_app \
--config generation.yaml \
--watch \
--verbose
# Generate only specific models
dart run vektagraf:generate \
--schema schema/app.json \
--output lib/generated/ \
--package-name my_app \
--models user,product,order
# Generate with custom templates
dart run vektagraf:generate \
--schema schema/app.json \
--output lib/generated/ \
--package-name my_app \
--templates custom_templates/
Programmatic Code Generation
For more control, use the programmatic API:
// tool/generate.dart
import 'dart:io';
import 'package:vektagraf/vektagraf.dart';
Future<void> main() async {
await generateCode();
}
Future<void> generateCode() async {
print('Starting code generation...');
try {
// Parse schema
final parser = VektagrafSchemaParser();
final schema = await parser.parseFromFile('schema/app.json');
// Validate schema
await validateSchema(schema);
// Configure generation
final config = CodeGenerationConfig(
outputDirectory: 'lib/generated',
packageName: 'my_app',
generateToString: true,
generateEquality: true,
generateCopyWith: true,
generateSerialization: true,
);
// Generate code
final generator = VektagrafCodeGenerator(config);
await generator.generateFromSchema(schema);
// Post-process generated files
await postProcessGeneration();
print('Code generation completed successfully!');
} catch (e, stackTrace) {
print('Code generation failed: $e');
print('Stack trace: $stackTrace');
exit(1);
}
}
Future<void> validateSchema(VektagrafSchema schema) async {
print('Validating schema...');
// Check for circular dependencies
final circularDeps = findCircularDependencies(schema);
if (circularDeps.isNotEmpty) {
throw Exception('Circular dependencies found: $circularDeps');
}
// Validate vector dimensions
for (final model in schema.models.values) {
for (final field in model.vectorFields) {
if (field.vectorConfig!.dimensions > 2048) {
throw Exception(
'Vector field ${model.name}.${field.name} has too many dimensions: '
'${field.vectorConfig!.dimensions} (max: 2048)'
);
}
}
}
print('Schema validation passed');
}
List<String> findCircularDependencies(VektagrafSchema schema) {
final dependencies = <String, Set<String>>{};
// Build dependency graph
for (final model in schema.models.values) {
dependencies[model.name] = {};
for (final field in model.relationshipFields) {
dependencies[model.name]!.add(field.relationshipConfig!.targetModel);
}
}
// Find cycles using DFS
final visited = <String>{};
final recursionStack = <String>{};
final cycles = <String>[];
void dfs(String node) {
visited.add(node);
recursionStack.add(node);
for (final neighbor in dependencies[node] ?? <String>{}) {
if (!visited.contains(neighbor)) {
dfs(neighbor);
} else if (recursionStack.contains(neighbor)) {
cycles.add('$node -> $neighbor');
}
}
recursionStack.remove(node);
}
for (final model in schema.models.keys) {
if (!visited.contains(model)) {
dfs(model);
}
}
return cycles;
}
Future<void> postProcessGeneration() async {
print('Post-processing generated files...');
// Format generated code
final result = await Process.run('dart', ['format', 'lib/generated/']);
if (result.exitCode != 0) {
print('Warning: Code formatting failed: ${result.stderr}');
}
// Run analysis
final analysisResult = await Process.run('dart', ['analyze', 'lib/generated/']);
if (analysisResult.exitCode != 0) {
print('Warning: Generated code has analysis issues: ${analysisResult.stdout}');
}
print('Post-processing completed');
}
Generation Configuration
Create a configuration file for consistent generation:
# generation.yaml
output_directory: lib/generated
package_name: my_app
# Code generation options
generate_toString: true
generate_equality: true
generate_copy_with: true
generate_serialization: true
generate_validation: true
# File organization
separate_files: true
barrel_file: true
extension_files: true
# Naming conventions
class_naming: PascalCase
field_naming: camelCase
method_naming: camelCase
file_naming: snake_case
# Documentation
generate_docs: true
include_examples: true
include_metadata: true
# Performance optimizations
lazy_loading: true
connection_pooling: true
batch_operations: true
# Security features
field_encryption: auto
access_control: auto
audit_logging: true
Generated Code Structure
Understanding the generated code structure helps you work effectively with Vektagraf.
Model Classes
Generated model classes are immutable and type-safe:
// Generated: lib/generated/user.dart
class User {
final VektagrafId id;
final String username;
final String email;
final String displayName;
final String? bio;
final List<double>? profileEmbedding;
final UserProfile? profile;
final List<Order> orders;
final Set<User> friends;
final DateTime createdAt;
final DateTime updatedAt;
const User({
required this.id,
required this.username,
required this.email,
required this.displayName,
this.bio,
this.profileEmbedding,
this.profile,
this.orders = const [],
this.friends = const {},
required this.createdAt,
required this.updatedAt,
});
// Validation
bool get isValid => _validate().isEmpty;
List<String> _validate() {
final errors = <String>[];
if (username.length < 3 || username.length > 50) {
errors.add('Username must be between 3 and 50 characters');
}
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(username)) {
errors.add('Username can only contain letters, numbers, and underscores');
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(email)) {
errors.add('Invalid email format');
}
return errors;
}
// Immutable updates
User copyWith({
String? username,
String? email,
String? displayName,
String? bio,
List<double>? profileEmbedding,
UserProfile? profile,
List<Order>? orders,
Set<User>? friends,
}) => User(
id: id,
username: username ?? this.username,
email: email ?? this.email,
displayName: displayName ?? this.displayName,
bio: bio ?? this.bio,
profileEmbedding: profileEmbedding ?? this.profileEmbedding,
profile: profile ?? this.profile,
orders: orders ?? this.orders,
friends: friends ?? this.friends,
createdAt: createdAt,
updatedAt: DateTime.now(),
);
// Serialization
Map<String, dynamic> toJson() => {
'id': id.toString(),
'username': username,
'email': email,
'display_name': displayName,
if (bio != null) 'bio': bio,
if (profileEmbedding != null) 'profile_embedding': profileEmbedding,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
factory User.fromJson(Map<String, dynamic> json) => User(
id: VektagrafId.parse(json['id']),
username: json['username'],
email: json['email'],
displayName: json['display_name'],
bio: json['bio'],
profileEmbedding: (json['profile_embedding'] as List?)?.cast<double>(),
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
);
// Equality and hashing
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is User &&
id == other.id &&
username == other.username &&
email == other.email &&
displayName == other.displayName &&
bio == other.bio &&
profileEmbedding == other.profileEmbedding &&
createdAt == other.createdAt &&
updatedAt == other.updatedAt;
@override
int get hashCode => Object.hash(
id, username, email, displayName, bio,
profileEmbedding, createdAt, updatedAt
);
@override
String toString() => 'User('
'id: $id, '
'username: $username, '
'email: $email, '
'displayName: $displayName, '
'bio: $bio, '
'createdAt: $createdAt, '
'updatedAt: $updatedAt'
')';
}
Database Extensions
Generated database extensions provide type-safe collection access:
// Generated: lib/generated/database_extensions.dart
extension AppDatabase on VektagrafDatabase {
/// Get collection of User objects
Future<VektagrafList<User>> users() => objects<User>();
/// Get collection of Order objects
Future<VektagrafList<Order>> orders() => objects<Order>();
/// Get collection of Product objects
Future<VektagrafList<Product>> products() => objects<Product>();
}
Vector Search Extensions
For models with vector fields, specialized search extensions are generated:
// Generated: lib/generated/vector_extensions.dart
extension UserVectorSearch on VektagrafList<User> {
/// Find users similar to the given profile embedding
Future<VektagrafList<User>> whereSimilarProfileEmbedding(
List<double> queryVector, {
int limit = 10,
double? threshold,
}) async {
if (queryVector.length != 384) {
throw ArgumentError(
'Query vector must have 384 dimensions, got ${queryVector.length}',
);
}
return whereSimilar('profileEmbedding', queryVector,
limit: limit, threshold: threshold);
}
/// Find users similar to another user's profile
Future<VektagrafList<User>> whereSimilarProfileEmbeddingTo(
User referenceUser, {
int limit = 10,
double? threshold,
}) async {
if (referenceUser.profileEmbedding == null) {
throw ArgumentError('Reference user must have a profile embedding');
}
return whereSimilarProfileEmbedding(
referenceUser.profileEmbedding!,
limit: limit,
threshold: threshold
);
}
/// Hybrid search: profile similarity + filtering
Future<VektagrafList<User>> whereSimilarProfileEmbeddingAndWhere(
List<double> queryVector,
bool Function(User) test, {
int limit = 10,
double? threshold,
}) async {
return (await whereSimilarProfileEmbedding(
queryVector,
limit: limit * 2,
threshold: threshold
)).where(test).take(limit);
}
}
Relationship Extensions
For models with relationships, traversal extensions are generated:
// Generated: lib/generated/relationship_extensions.dart
extension UserRelationships on VektagrafList<User> {
/// Expand orders relationships
Future<VektagrafList<Order>> expandOrders() async {
final results = <Order>[];
for (final user in this) {
if (user.orders.isNotEmpty) {
results.addAll(user.orders);
}
}
return VektagrafList<Order>.from(results);
}
/// Expand orders with filtering
Future<VektagrafList<Order>> expandOrdersWhere(
bool Function(Order) test,
) async {
return (await expandOrders()).where(test);
}
/// Filter users who have orders
VektagrafList<User> whereHasOrders() {
return where((user) => user.orders.isNotEmpty);
}
/// Filter users based on their orders
VektagrafList<User> whereRelatedOrders(
bool Function(Order) test,
) {
return where((user) => user.orders.any(test));
}
/// Filter users by order count
VektagrafList<User> whereCountOrders(
int count, {
bool Function(int actual, int expected)? comparison,
}) {
final compare = comparison ?? (actual, expected) => actual == expected;
return where((user) => compare(user.orders.length, count));
}
/// Filter users with at least [count] orders
VektagrafList<User> whereAtLeastOrders(int count) {
return whereCountOrders(count,
comparison: (actual, expected) => actual >= expected);
}
}
Schema Evolution and Migration
As your application grows, your schema will need to evolve. Vektagraf provides tools and patterns for safe schema evolution.
Versioning Strategy
Use semantic versioning for your schemas:
{
"$schema": "vektagraf://schema/v1",
"version": "2.1.0",
"compatibility": {
"minimum_version": "2.0.0",
"breaking_changes": ["removed_field_user.old_field"],
"deprecations": ["user.legacy_field"]
},
"migration": {
"from_version": "2.0.0",
"migration_script": "migrations/v2_0_to_v2_1.dart"
}
}
Backward Compatible Changes
These changes don't break existing code:
Adding Optional Fields
{
"user": {
"id": "id",
"username": "string",
"email": "string",
// New optional field - backward compatible
"phone": {
"type": "string",
"optional": true,
"validation": {
"pattern": "^\\+?[1-9]\\d{1,14}$"
}
}
}
}
Adding New Models
{
"models": {
"user": {
// Existing model unchanged
"id": "id",
"username": "string",
"email": "string"
},
// New model - backward compatible
"notification": {
"id": "id",
"user_id": "id",
"message": "string",
"read": "bool",
"created_at": "datetime"
}
}
}
Relaxing Validation
{
"user": {
"username": {
"type": "string",
"validation": {
"minLength": 2, // Was 3 - more permissive
"maxLength": 100 // Was 50 - more permissive
}
}
}
}
Breaking Changes
These changes require migration:
Removing Fields
{
"user": {
"id": "id",
"username": "string",
"email": "string"
// "old_field": "string" - REMOVED (breaking change)
}
}
Changing Field Types
{
"user": {
"id": "id",
"username": "string",
"age": "string" // Was "int32" - breaking change
}
}
Making Fields Required
{
"user": {
"id": "id",
"username": "string",
"email": "string",
"phone": "string" // Was optional - breaking change
}
}
Migration Scripts
Create migration scripts for breaking changes:
// migrations/v2_0_to_v2_1.dart
import 'package:vektagraf/vektagraf.dart';
class MigrationV2_0ToV2_1 {
static Future<void> migrate(VektagrafDatabase db) async {
print('Starting migration from v2.0 to v2.1...');
await db.transaction((tx) async {
// Migrate user data
await _migrateUsers(tx);
// Add new indexes
await _addIndexes(tx);
// Clean up old data
await _cleanup(tx);
});
print('Migration completed successfully');
}
static Future<void> _migrateUsers(VektagrafTransaction tx) async {
final users = await tx.users().toList();
for (final user in users) {
// Convert age from int to string
final migratedUser = user.copyWith(
// age: user.age?.toString(), // Convert int to string
);
await tx.users().save(migratedUser);
}
print('Migrated ${users.length} users');
}
static Future<void> _addIndexes(VektagrafTransaction tx) async {
// Add new indexes for performance
await tx.createIndex('user', ['phone']);
await tx.createIndex('notification', ['user_id', 'created_at']);
print('Added new indexes');
}
static Future<void> _cleanup(VektagrafTransaction tx) async {
// Remove deprecated data
// await tx.dropField('user', 'old_field');
print('Cleanup completed');
}
}
Migration Execution
// tool/migrate.dart
import 'package:vektagraf/vektagraf.dart';
import '../migrations/v2_0_to_v2_1.dart';
Future<void> main(List<String> args) async {
if (args.isEmpty) {
print('Usage: dart run tool/migrate.dart <database_path>');
return;
}
final dbPath = args[0];
final db = VektagrafDatabase();
try {
await db.open(dbPath);
// Check current schema version
final currentVersion = await getCurrentSchemaVersion(db);
print('Current schema version: $currentVersion');
// Determine required migrations
final migrations = getRequiredMigrations(currentVersion, '2.1.0');
if (migrations.isEmpty) {
print('No migrations required');
return;
}
// Create backup before migration
await createBackup(db, '$dbPath.backup.${DateTime.now().millisecondsSinceEpoch}');
// Execute migrations
for (final migration in migrations) {
print('Executing migration: ${migration.name}');
await migration.execute(db);
await updateSchemaVersion(db, migration.targetVersion);
}
print('All migrations completed successfully');
} catch (e, stackTrace) {
print('Migration failed: $e');
print('Stack trace: $stackTrace');
// Restore from backup if needed
await restoreFromBackup(db, '$dbPath.backup');
} finally {
await db.close();
}
}
Future<String> getCurrentSchemaVersion(VektagrafDatabase db) async {
// Implementation to get current schema version
return '2.0.0';
}
List<Migration> getRequiredMigrations(String from, String to) {
// Implementation to determine required migrations
return [
Migration('v2.0 to v2.1', '2.1.0', MigrationV2_0ToV2_1.migrate),
];
}
class Migration {
final String name;
final String targetVersion;
final Future<void> Function(VektagrafDatabase) execute;
Migration(this.name, this.targetVersion, this.execute);
}
Advanced Schema Patterns
Inheritance and Composition
Single Table Inheritance
{
"models": {
"content": {
"id": "id",
"title": "string",
"body": "string",
"type": {
"type": "string",
"enum": ["article", "video", "podcast"]
},
"created_at": "datetime",
// Article-specific fields
"word_count": {
"type": "int32",
"optional": true
},
// Video-specific fields
"duration_seconds": {
"type": "int32",
"optional": true
},
"video_url": {
"type": "string",
"optional": true
},
// Podcast-specific fields
"audio_url": {
"type": "string",
"optional": true
},
"episode_number": {
"type": "int32",
"optional": true
}
}
}
}
Composition Pattern
{
"models": {
"content": {
"id": "id",
"title": "string",
"body": "string",
"metadata": "@content_metadata",
"created_at": "datetime"
},
"content_metadata": {
"id": "id",
"type": {
"type": "string",
"enum": ["article", "video", "podcast"]
},
"article_data": "@article_metadata",
"video_data": "@video_metadata",
"podcast_data": "@podcast_metadata"
},
"article_metadata": {
"id": "id",
"word_count": "int32",
"reading_time": "int32"
},
"video_metadata": {
"id": "id",
"duration_seconds": "int32",
"resolution": "string",
"video_url": "string"
},
"podcast_metadata": {
"id": "id",
"duration_seconds": "int32",
"episode_number": "int32",
"audio_url": "string"
}
}
}
Multi-Tenant Schemas
{
"models": {
"tenant": {
"id": "id",
"name": "string",
"subdomain": "string",
"settings": "<string, dynamic>",
"created_at": "datetime"
},
"user": {
"$meta": {
"multi_tenant": {
"tenant_field": "tenant_id",
"isolation": "strict"
}
},
"id": "id",
"tenant_id": "id",
"tenant": "@tenant",
"username": "string",
"email": "string"
},
"document": {
"$meta": {
"multi_tenant": {
"tenant_field": "tenant_id",
"isolation": "strict"
}
},
"id": "id",
"tenant_id": "id",
"tenant": "@tenant",
"title": "string",
"content": "string",
"author": "@user"
}
}
}
Audit and Versioning
{
"models": {
"document": {
"$meta": {
"versioned": true,
"audit": true
},
"id": "id",
"title": "string",
"content": "string",
"version": "int32",
"created_at": "datetime",
"updated_at": "datetime",
"created_by": "@user",
"updated_by": "@user"
},
"document_version": {
"id": "id",
"document_id": "id",
"document": "@document",
"version": "int32",
"title": "string",
"content": "string",
"created_at": "datetime",
"created_by": "@user",
"change_summary": "string"
},
"audit_log": {
"id": "id",
"entity_type": "string",
"entity_id": "id",
"action": {
"type": "string",
"enum": ["create", "update", "delete", "read"]
},
"changes": "<string, dynamic>",
"user_id": "id",
"user": "@user",
"timestamp": "datetime",
"ip_address": "string",
"user_agent": "string"
}
}
}
Performance Optimization Patterns
Denormalization for Read Performance
{
"models": {
"user": {
"id": "id",
"username": "string",
"email": "string",
// Denormalized counters for performance
"post_count": "int32",
"follower_count": "int32",
"following_count": "int32"
},
"post": {
"id": "id",
"title": "string",
"content": "string",
"author": "@user",
// Denormalized author info for performance
"author_username": "string",
"author_display_name": "string",
// Denormalized counters
"like_count": "int32",
"comment_count": "int32",
"view_count": "int32"
}
}
}
Materialized Views
{
"models": {
"user_stats": {
"$meta": {
"materialized_view": true,
"refresh_interval": "1h",
"source_models": ["user", "post", "comment", "like"]
},
"user_id": "id",
"user": "@user",
"total_posts": "int32",
"total_comments": "int32",
"total_likes_received": "int32",
"total_likes_given": "int32",
"engagement_score": "float64",
"last_activity": "datetime",
"computed_at": "datetime"
}
}
}
Summary
Schema design is the foundation of successful Vektagraf applications. This chapter covered:
- JSON Schema Format: Complete structure and metadata organization
- Field Types: Comprehensive coverage of all supported types and validation
- Code Generation: Both CLI and programmatic approaches with configuration
- Generated Code: Understanding the structure and capabilities of generated code
- Schema Evolution: Strategies for safe schema changes and migrations
- Advanced Patterns: Inheritance, multi-tenancy, auditing, and performance optimization
Key takeaways:
- Design for evolution: Plan for schema changes from the beginning
- Use validation extensively: Catch errors early with comprehensive validation rules
- Leverage code generation: Let Vektagraf generate type-safe, optimized code
- Plan migrations carefully: Use versioning and migration scripts for breaking changes
- Apply patterns appropriately: Choose the right patterns for your use case
- Optimize for performance: Use denormalization and materialized views when needed
In the next chapter, we'll explore database operations and transactions, building on the solid foundation of well-designed schemas.
Next Steps
- Chapter 4: Database Operations and Transactions - Master advanced query patterns
- Chapter 5: Vector Search and Similarity Operations - Implement semantic search
- Chapter 6: Graph Operations and Relationship Modeling - Build connected applications
- Chapter 7: Query Optimization and Performance - Optimize for scale