dbos 1.13.0a3__py3-none-any.whl → 1.13.0a6__py3-none-any.whl
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.
- dbos/_app_db.py +215 -80
- dbos/_client.py +30 -15
- dbos/_core.py +4 -8
- dbos/_dbos.py +4 -12
- dbos/_dbos_config.py +118 -50
- dbos/_migration.py +89 -0
- dbos/_sys_db.py +121 -169
- dbos/_sys_db_postgres.py +173 -0
- dbos/_sys_db_sqlite.py +182 -0
- dbos/_utils.py +10 -1
- dbos/cli/cli.py +203 -94
- dbos/cli/migration.py +2 -2
- dbos/dbos-config.schema.json +4 -0
- {dbos-1.13.0a3.dist-info → dbos-1.13.0a6.dist-info}/METADATA +1 -1
- {dbos-1.13.0a3.dist-info → dbos-1.13.0a6.dist-info}/RECORD +18 -17
- dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -67
- {dbos-1.13.0a3.dist-info → dbos-1.13.0a6.dist-info}/WHEEL +0 -0
- {dbos-1.13.0a3.dist-info → dbos-1.13.0a6.dist-info}/entry_points.txt +0 -0
- {dbos-1.13.0a3.dist-info → dbos-1.13.0a6.dist-info}/licenses/LICENSE +0 -0
dbos/_dbos_config.py
CHANGED
|
@@ -22,9 +22,10 @@ class DBOSConfig(TypedDict, total=False):
|
|
|
22
22
|
|
|
23
23
|
Attributes:
|
|
24
24
|
name (str): Application name
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
system_database_url (str): Connection string for the DBOS system database. Defaults to sqlite:///{name} if not provided.
|
|
26
|
+
application_database_url (str): Connection string for the DBOS application database, in which DBOS @Transaction functions run. Optional. Should be the same type of database (SQLite or Postgres) as the system database.
|
|
27
|
+
database_url (str): (DEPRECATED) Database connection string
|
|
28
|
+
sys_db_name (str): (DEPRECATED) System database name
|
|
28
29
|
sys_db_pool_size (int): System database pool size
|
|
29
30
|
db_engine_kwargs (Dict[str, Any]): SQLAlchemy engine kwargs (See https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine)
|
|
30
31
|
log_level (str): Log level
|
|
@@ -39,8 +40,9 @@ class DBOSConfig(TypedDict, total=False):
|
|
|
39
40
|
"""
|
|
40
41
|
|
|
41
42
|
name: str
|
|
42
|
-
database_url: Optional[str]
|
|
43
43
|
system_database_url: Optional[str]
|
|
44
|
+
application_database_url: Optional[str]
|
|
45
|
+
database_url: Optional[str]
|
|
44
46
|
sys_db_name: Optional[str]
|
|
45
47
|
sys_db_pool_size: Optional[int]
|
|
46
48
|
db_engine_kwargs: Optional[Dict[str, Any]]
|
|
@@ -107,12 +109,12 @@ class ConfigFile(TypedDict, total=False):
|
|
|
107
109
|
|
|
108
110
|
Attributes:
|
|
109
111
|
name (str): Application name
|
|
110
|
-
runtimeConfig (RuntimeConfig): Configuration for
|
|
112
|
+
runtimeConfig (RuntimeConfig): Configuration for DBOS Cloud
|
|
111
113
|
database (DatabaseConfig): Configure pool sizes, migrate commands
|
|
112
|
-
database_url (str):
|
|
114
|
+
database_url (str): Application database URL
|
|
115
|
+
system_database_url (str): System database URL
|
|
113
116
|
telemetry (TelemetryConfig): Configuration for tracing / logging
|
|
114
|
-
env (Dict[str,str]): Environment
|
|
115
|
-
application (Dict[str, Any]): Application-specific configuration section
|
|
117
|
+
env (Dict[str,str]): Environment variables
|
|
116
118
|
|
|
117
119
|
"""
|
|
118
120
|
|
|
@@ -144,8 +146,12 @@ def translate_dbos_config_to_config_file(config: DBOSConfig) -> ConfigFile:
|
|
|
144
146
|
if db_config:
|
|
145
147
|
translated_config["database"] = db_config
|
|
146
148
|
|
|
149
|
+
# Use application_database_url instead of the deprecated database_url if provided
|
|
147
150
|
if "database_url" in config:
|
|
148
151
|
translated_config["database_url"] = config.get("database_url")
|
|
152
|
+
elif "application_database_url" in config:
|
|
153
|
+
translated_config["database_url"] = config.get("application_database_url")
|
|
154
|
+
|
|
149
155
|
if "system_database_url" in config:
|
|
150
156
|
translated_config["system_database_url"] = config.get("system_database_url")
|
|
151
157
|
|
|
@@ -233,7 +239,6 @@ def _substitute_env_vars(content: str, silent: bool = False) -> str:
|
|
|
233
239
|
def load_config(
|
|
234
240
|
config_file_path: str = DBOS_CONFIG_PATH,
|
|
235
241
|
*,
|
|
236
|
-
run_process_config: bool = True,
|
|
237
242
|
silent: bool = False,
|
|
238
243
|
) -> ConfigFile:
|
|
239
244
|
"""
|
|
@@ -283,8 +288,6 @@ def load_config(
|
|
|
283
288
|
]
|
|
284
289
|
|
|
285
290
|
data = cast(ConfigFile, data)
|
|
286
|
-
if run_process_config:
|
|
287
|
-
data = process_config(data=data, silent=silent)
|
|
288
291
|
return data # type: ignore
|
|
289
292
|
|
|
290
293
|
|
|
@@ -296,19 +299,12 @@ def process_config(
|
|
|
296
299
|
"""
|
|
297
300
|
If a database_url is provided, pass it as is in the config.
|
|
298
301
|
|
|
299
|
-
Else,
|
|
302
|
+
Else, default to SQLite.
|
|
300
303
|
|
|
301
304
|
Also build SQL Alchemy "kwargs" base on user input + defaults.
|
|
302
305
|
Specifically, db_engine_kwargs takes precedence over app_db_pool_size
|
|
303
306
|
|
|
304
307
|
In debug mode, apply overrides from DBOS_DBHOST, DBOS_DBPORT, DBOS_DBUSER, and DBOS_DBPASSWORD.
|
|
305
|
-
|
|
306
|
-
Default configuration:
|
|
307
|
-
- Hostname: localhost
|
|
308
|
-
- Port: 5432
|
|
309
|
-
- Username: postgres
|
|
310
|
-
- Password: $PGPASSWORD
|
|
311
|
-
- Database name: transformed application name.
|
|
312
308
|
"""
|
|
313
309
|
|
|
314
310
|
if "name" not in data:
|
|
@@ -340,20 +336,16 @@ def process_config(
|
|
|
340
336
|
|
|
341
337
|
# Ensure database dict exists
|
|
342
338
|
data.setdefault("database", {})
|
|
343
|
-
|
|
344
|
-
# Database URL resolution
|
|
345
339
|
connect_timeout = None
|
|
346
|
-
|
|
340
|
+
|
|
341
|
+
# Process the application database URL, if provided
|
|
342
|
+
if data.get("database_url"):
|
|
347
343
|
# Parse the db string and check required fields
|
|
348
344
|
assert data["database_url"] is not None
|
|
349
345
|
assert is_valid_database_url(data["database_url"])
|
|
350
346
|
|
|
351
347
|
url = make_url(data["database_url"])
|
|
352
348
|
|
|
353
|
-
if not data["database"].get("sys_db_name"):
|
|
354
|
-
assert url.database is not None
|
|
355
|
-
data["database"]["sys_db_name"] = url.database + SystemSchema.sysdb_suffix
|
|
356
|
-
|
|
357
349
|
# Gather connect_timeout from the URL if provided. It should be used in engine kwargs if not provided there (instead of our default)
|
|
358
350
|
connect_timeout_str = url.query.get("connect_timeout")
|
|
359
351
|
if connect_timeout_str is not None:
|
|
@@ -378,25 +370,88 @@ def process_config(
|
|
|
378
370
|
host=os.getenv("DBOS_DBHOST", url.host),
|
|
379
371
|
port=port,
|
|
380
372
|
).render_as_string(hide_password=False)
|
|
381
|
-
|
|
373
|
+
|
|
374
|
+
# Process the system database URL, if provided
|
|
375
|
+
if data.get("system_database_url"):
|
|
376
|
+
# Parse the db string and check required fields
|
|
377
|
+
assert data["system_database_url"]
|
|
378
|
+
assert is_valid_database_url(data["system_database_url"])
|
|
379
|
+
|
|
380
|
+
url = make_url(data["system_database_url"])
|
|
381
|
+
|
|
382
|
+
# Gather connect_timeout from the URL if provided. It should be used in engine kwargs if not provided there (instead of our default). This overrides a timeout from the application database, if any.
|
|
383
|
+
connect_timeout_str = url.query.get("connect_timeout")
|
|
384
|
+
if connect_timeout_str is not None:
|
|
385
|
+
assert isinstance(
|
|
386
|
+
connect_timeout_str, str
|
|
387
|
+
), "connect_timeout must be a string and defined once in the URL"
|
|
388
|
+
if connect_timeout_str.isdigit():
|
|
389
|
+
connect_timeout = int(connect_timeout_str)
|
|
390
|
+
|
|
391
|
+
# In debug mode perform env vars overrides
|
|
392
|
+
if isDebugMode:
|
|
393
|
+
# Override the username, password, host, and port
|
|
394
|
+
port_str = os.getenv("DBOS_DBPORT")
|
|
395
|
+
port = (
|
|
396
|
+
int(port_str)
|
|
397
|
+
if port_str is not None and port_str.isdigit()
|
|
398
|
+
else url.port
|
|
399
|
+
)
|
|
400
|
+
data["system_database_url"] = url.set(
|
|
401
|
+
username=os.getenv("DBOS_DBUSER", url.username),
|
|
402
|
+
password=os.getenv("DBOS_DBPASSWORD", url.password),
|
|
403
|
+
host=os.getenv("DBOS_DBHOST", url.host),
|
|
404
|
+
port=port,
|
|
405
|
+
).render_as_string(hide_password=False)
|
|
406
|
+
|
|
407
|
+
# If an application database URL is provided but not the system database URL,
|
|
408
|
+
# construct the system database URL.
|
|
409
|
+
if data.get("database_url") and not data.get("system_database_url"):
|
|
410
|
+
assert data["database_url"]
|
|
411
|
+
if data["database_url"].startswith("sqlite"):
|
|
412
|
+
data["system_database_url"] = data["database_url"]
|
|
413
|
+
else:
|
|
414
|
+
url = make_url(data["database_url"])
|
|
415
|
+
assert url.database
|
|
416
|
+
if data["database"].get("sys_db_name"):
|
|
417
|
+
url = url.set(database=data["database"]["sys_db_name"])
|
|
418
|
+
else:
|
|
419
|
+
url = url.set(database=f"{url.database}{SystemSchema.sysdb_suffix}")
|
|
420
|
+
data["system_database_url"] = url.render_as_string(hide_password=False)
|
|
421
|
+
|
|
422
|
+
# If a system database URL is provided but not an application database URL, set the
|
|
423
|
+
# application database URL to the system database URL.
|
|
424
|
+
if data.get("system_database_url") and not data.get("database_url"):
|
|
425
|
+
assert data["system_database_url"]
|
|
426
|
+
data["database_url"] = data["system_database_url"]
|
|
427
|
+
|
|
428
|
+
# If neither URL is provided, use a default SQLite database URL.
|
|
429
|
+
if not data.get("database_url") and not data.get("system_database_url"):
|
|
382
430
|
_app_db_name = _app_name_to_db_name(data["name"])
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
f"postgres://postgres:{_password}@localhost:5432/{_app_db_name}?connect_timeout=10&sslmode=prefer"
|
|
431
|
+
data["system_database_url"] = data["database_url"] = (
|
|
432
|
+
f"sqlite:///{_app_db_name}.sqlite"
|
|
386
433
|
)
|
|
387
|
-
if not data["database"].get("sys_db_name"):
|
|
388
|
-
data["database"]["sys_db_name"] = _app_db_name + SystemSchema.sysdb_suffix
|
|
389
|
-
assert data["database_url"] is not None
|
|
390
434
|
|
|
391
435
|
configure_db_engine_parameters(data["database"], connect_timeout=connect_timeout)
|
|
392
436
|
|
|
393
|
-
|
|
437
|
+
assert data["database_url"] is not None
|
|
438
|
+
assert data["system_database_url"] is not None
|
|
439
|
+
# Pretty-print connection information, respecting log level
|
|
394
440
|
if not silent and logs["logLevel"] == "INFO" or logs["logLevel"] == "DEBUG":
|
|
395
|
-
|
|
396
|
-
|
|
441
|
+
printable_sys_db_url = make_url(data["system_database_url"]).render_as_string(
|
|
442
|
+
hide_password=True
|
|
443
|
+
)
|
|
397
444
|
print(
|
|
398
|
-
f"[bold blue]
|
|
445
|
+
f"[bold blue]DBOS system database URL: {printable_sys_db_url}[/bold blue]"
|
|
399
446
|
)
|
|
447
|
+
if data["database_url"].startswith("sqlite"):
|
|
448
|
+
print(
|
|
449
|
+
f"[bold blue]Using SQLite as a system database. The SQLite system database is for development and testing. PostgreSQL is recommended for production use.[/bold blue]"
|
|
450
|
+
)
|
|
451
|
+
else:
|
|
452
|
+
print(
|
|
453
|
+
f"[bold blue]Database engine parameters: {data['database']['db_engine_kwargs']}[/bold blue]"
|
|
454
|
+
)
|
|
400
455
|
|
|
401
456
|
# Return data as ConfigFile type
|
|
402
457
|
return data
|
|
@@ -445,6 +500,8 @@ def configure_db_engine_parameters(
|
|
|
445
500
|
|
|
446
501
|
|
|
447
502
|
def is_valid_database_url(database_url: str) -> bool:
|
|
503
|
+
if database_url.startswith("sqlite"):
|
|
504
|
+
return True
|
|
448
505
|
url = make_url(database_url)
|
|
449
506
|
required_fields = [
|
|
450
507
|
("username", "Username must be specified in the connection URL"),
|
|
@@ -473,36 +530,34 @@ def _app_name_to_db_name(app_name: str) -> str:
|
|
|
473
530
|
|
|
474
531
|
def overwrite_config(provided_config: ConfigFile) -> ConfigFile:
|
|
475
532
|
# Load the DBOS configuration file and force the use of:
|
|
476
|
-
# 1. The database url provided by DBOS_DATABASE_URL
|
|
533
|
+
# 1. The application and system database url provided by DBOS_DATABASE_URL and DBOS_SYSTEM_DATABASE_URL
|
|
477
534
|
# 2. OTLP traces endpoints (add the config data to the provided config)
|
|
478
535
|
# 3. Use the application name from the file. This is a defensive measure to ensure the application name is whatever it was registered with in the cloud
|
|
479
536
|
# 4. Remove admin_port is provided in code
|
|
480
537
|
# 5. Remove env vars if provided in code
|
|
481
538
|
# Optimistically assume that expected fields in config_from_file are present
|
|
482
539
|
|
|
483
|
-
config_from_file = load_config(
|
|
540
|
+
config_from_file = load_config()
|
|
484
541
|
# Be defensive
|
|
485
542
|
if config_from_file is None:
|
|
486
543
|
return provided_config
|
|
487
544
|
|
|
488
|
-
#
|
|
545
|
+
# Set the application name to the cloud app name
|
|
489
546
|
provided_config["name"] = config_from_file["name"]
|
|
490
547
|
|
|
491
|
-
#
|
|
492
|
-
if "database" not in provided_config:
|
|
493
|
-
provided_config["database"] = {}
|
|
494
|
-
provided_config["database"]["sys_db_name"] = config_from_file["database"][
|
|
495
|
-
"sys_db_name"
|
|
496
|
-
]
|
|
497
|
-
|
|
548
|
+
# Use the DBOS Cloud application and system database URLs
|
|
498
549
|
db_url = os.environ.get("DBOS_DATABASE_URL")
|
|
499
550
|
if db_url is None:
|
|
500
551
|
raise DBOSInitializationError(
|
|
501
552
|
"DBOS_DATABASE_URL environment variable is not set. This is required to connect to the database."
|
|
502
553
|
)
|
|
503
554
|
provided_config["database_url"] = db_url
|
|
504
|
-
|
|
505
|
-
|
|
555
|
+
system_db_url = os.environ.get("DBOS_SYSTEM_DATABASE_URL")
|
|
556
|
+
if system_db_url is None:
|
|
557
|
+
raise DBOSInitializationError(
|
|
558
|
+
"DBOS_SYSTEM_DATABASE_URL environment variable is not set. This is required to connect to the database."
|
|
559
|
+
)
|
|
560
|
+
provided_config["system_database_url"] = system_db_url
|
|
506
561
|
|
|
507
562
|
# Telemetry config
|
|
508
563
|
if "telemetry" not in provided_config or provided_config["telemetry"] is None:
|
|
@@ -559,8 +614,10 @@ def get_system_database_url(config: ConfigFile) -> str:
|
|
|
559
614
|
return config["system_database_url"]
|
|
560
615
|
else:
|
|
561
616
|
assert config["database_url"] is not None
|
|
617
|
+
if config["database_url"].startswith("sqlite"):
|
|
618
|
+
return config["database_url"]
|
|
562
619
|
app_db_url = make_url(config["database_url"])
|
|
563
|
-
if config["database"].get("sys_db_name") is not None:
|
|
620
|
+
if config.get("database") and config["database"].get("sys_db_name") is not None:
|
|
564
621
|
sys_db_name = config["database"]["sys_db_name"]
|
|
565
622
|
else:
|
|
566
623
|
assert app_db_url.database is not None
|
|
@@ -568,3 +625,14 @@ def get_system_database_url(config: ConfigFile) -> str:
|
|
|
568
625
|
return app_db_url.set(database=sys_db_name).render_as_string(
|
|
569
626
|
hide_password=False
|
|
570
627
|
)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def get_application_database_url(config: ConfigFile) -> str:
|
|
631
|
+
# For backwards compatibility, the application database URL is "database_url"
|
|
632
|
+
if config.get("database_url"):
|
|
633
|
+
assert config["database_url"]
|
|
634
|
+
return config["database_url"]
|
|
635
|
+
else:
|
|
636
|
+
# If the application database URL is not specified, set it to the system database URL
|
|
637
|
+
assert config["system_database_url"]
|
|
638
|
+
return config["system_database_url"]
|
dbos/_migration.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
|
+
import sys
|
|
4
5
|
|
|
5
6
|
import sqlalchemy as sa
|
|
6
7
|
from alembic import command
|
|
@@ -230,4 +231,92 @@ CREATE TABLE dbos.event_dispatch_kv (
|
|
|
230
231
|
);
|
|
231
232
|
"""
|
|
232
233
|
|
|
234
|
+
|
|
235
|
+
def get_sqlite_timestamp_expr() -> str:
|
|
236
|
+
"""Get SQLite timestamp expression with millisecond precision for Python >= 3.12."""
|
|
237
|
+
if sys.version_info >= (3, 12):
|
|
238
|
+
return "(unixepoch('subsec') * 1000)"
|
|
239
|
+
else:
|
|
240
|
+
return "(strftime('%s','now') * 1000)"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
sqlite_migration_one = f"""
|
|
244
|
+
CREATE TABLE workflow_status (
|
|
245
|
+
workflow_uuid TEXT PRIMARY KEY,
|
|
246
|
+
status TEXT,
|
|
247
|
+
name TEXT,
|
|
248
|
+
authenticated_user TEXT,
|
|
249
|
+
assumed_role TEXT,
|
|
250
|
+
authenticated_roles TEXT,
|
|
251
|
+
request TEXT,
|
|
252
|
+
output TEXT,
|
|
253
|
+
error TEXT,
|
|
254
|
+
executor_id TEXT,
|
|
255
|
+
created_at INTEGER NOT NULL DEFAULT {get_sqlite_timestamp_expr()},
|
|
256
|
+
updated_at INTEGER NOT NULL DEFAULT {get_sqlite_timestamp_expr()},
|
|
257
|
+
application_version TEXT,
|
|
258
|
+
application_id TEXT,
|
|
259
|
+
class_name TEXT DEFAULT NULL,
|
|
260
|
+
config_name TEXT DEFAULT NULL,
|
|
261
|
+
recovery_attempts INTEGER DEFAULT 0,
|
|
262
|
+
queue_name TEXT,
|
|
263
|
+
workflow_timeout_ms INTEGER,
|
|
264
|
+
workflow_deadline_epoch_ms INTEGER,
|
|
265
|
+
inputs TEXT,
|
|
266
|
+
started_at_epoch_ms INTEGER,
|
|
267
|
+
deduplication_id TEXT,
|
|
268
|
+
priority INTEGER NOT NULL DEFAULT 0
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
CREATE INDEX workflow_status_created_at_index ON workflow_status (created_at);
|
|
272
|
+
CREATE INDEX workflow_status_executor_id_index ON workflow_status (executor_id);
|
|
273
|
+
CREATE INDEX workflow_status_status_index ON workflow_status (status);
|
|
274
|
+
|
|
275
|
+
CREATE UNIQUE INDEX uq_workflow_status_queue_name_dedup_id
|
|
276
|
+
ON workflow_status (queue_name, deduplication_id);
|
|
277
|
+
|
|
278
|
+
CREATE TABLE operation_outputs (
|
|
279
|
+
workflow_uuid TEXT NOT NULL,
|
|
280
|
+
function_id INTEGER NOT NULL,
|
|
281
|
+
function_name TEXT NOT NULL DEFAULT '',
|
|
282
|
+
output TEXT,
|
|
283
|
+
error TEXT,
|
|
284
|
+
child_workflow_id TEXT,
|
|
285
|
+
PRIMARY KEY (workflow_uuid, function_id),
|
|
286
|
+
FOREIGN KEY (workflow_uuid) REFERENCES workflow_status(workflow_uuid)
|
|
287
|
+
ON UPDATE CASCADE ON DELETE CASCADE
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
CREATE TABLE notifications (
|
|
291
|
+
destination_uuid TEXT NOT NULL,
|
|
292
|
+
topic TEXT,
|
|
293
|
+
message TEXT NOT NULL,
|
|
294
|
+
created_at_epoch_ms INTEGER NOT NULL DEFAULT {get_sqlite_timestamp_expr()},
|
|
295
|
+
message_uuid TEXT NOT NULL DEFAULT (hex(randomblob(16))),
|
|
296
|
+
FOREIGN KEY (destination_uuid) REFERENCES workflow_status(workflow_uuid)
|
|
297
|
+
ON UPDATE CASCADE ON DELETE CASCADE
|
|
298
|
+
);
|
|
299
|
+
CREATE INDEX idx_workflow_topic ON notifications (destination_uuid, topic);
|
|
300
|
+
|
|
301
|
+
CREATE TABLE workflow_events (
|
|
302
|
+
workflow_uuid TEXT NOT NULL,
|
|
303
|
+
key TEXT NOT NULL,
|
|
304
|
+
value TEXT NOT NULL,
|
|
305
|
+
PRIMARY KEY (workflow_uuid, key),
|
|
306
|
+
FOREIGN KEY (workflow_uuid) REFERENCES workflow_status(workflow_uuid)
|
|
307
|
+
ON UPDATE CASCADE ON DELETE CASCADE
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
CREATE TABLE streams (
|
|
311
|
+
workflow_uuid TEXT NOT NULL,
|
|
312
|
+
key TEXT NOT NULL,
|
|
313
|
+
value TEXT NOT NULL,
|
|
314
|
+
"offset" INTEGER NOT NULL,
|
|
315
|
+
PRIMARY KEY (workflow_uuid, key, "offset"),
|
|
316
|
+
FOREIGN KEY (workflow_uuid) REFERENCES workflow_status(workflow_uuid)
|
|
317
|
+
ON UPDATE CASCADE ON DELETE CASCADE
|
|
318
|
+
);
|
|
319
|
+
"""
|
|
320
|
+
|
|
233
321
|
dbos_migrations = [dbos_migration_one]
|
|
322
|
+
sqlite_migrations = [sqlite_migration_one]
|