Add order column to bw_selects and update migration scripts for version 1.6.0-rc2

This commit is contained in:
Théophile Diot 2025-01-16 09:48:52 +01:00
parent 51f6cf570d
commit 87fdcb2ece
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
12 changed files with 391 additions and 61 deletions

View file

@ -75,6 +75,7 @@ jq -r 'to_entries[] | "\(.key) \(.value)"' databases.json | while read -r databa
# Start the database stack if not SQLite
if [[ "$database" != "sqlite" ]]; then
log "🚀 Starting Docker stack for $database"
docker compose -f "$database.yml" pull || true
if ! docker compose -f "$database.yml" up -d; then
log "❌ Failed to start the Docker stack for $database"
docker compose down -v --remove-orphans

View file

@ -1,3 +1,6 @@
{
"sqlite": "sqlite:////var/lib/bunkerweb/db.sqlite3",
"mariadb": "mariadb+pymysql://bunkerweb:secret@bw-db:3306/db",
"mysql": "mysql+pymysql://bunkerweb:secret@bw-db:3306/db",
"postgresql": "postgresql+psycopg://bunkerweb:secret@bw-db:5432/db"
}

View file

@ -15,6 +15,7 @@ from re import Match, compile as re_compile, escape, error as RegexError, search
from sys import argv, path as sys_path
from tarfile import open as tar_open
from threading import Lock
from traceback import format_exc
from typing import Any, Dict, List, Literal, Optional, Set, Tuple, Union
from time import sleep
from uuid import uuid4
@ -605,7 +606,7 @@ class Database:
# For selects:
old_selects = {}
for sel in old_data.get("bw_selects", []):
old_selects[(sel.setting_id, sel.value)] = sel
old_selects[(sel.setting_id, sel.value, sel.order)] = sel
# For jobs:
old_jobs = {}
@ -628,11 +629,11 @@ class Database:
old_template_settings = {}
for ts in old_data.get("bw_template_settings", []):
old_template_settings[(ts.template_id, ts.setting_id, ts.suffix, ts.step_id)] = ts
old_template_settings[(ts.template_id, ts.setting_id, ts.suffix, ts.step_id, ts.order)] = ts.default
old_template_configs = {}
for tc in old_data.get("bw_template_custom_configs", []):
old_template_configs[(tc.template_id, tc.type, tc.name, tc.step_id)] = tc
old_template_configs[(tc.template_id, tc.type, tc.name, tc.step_id, ts.order)] = tc
# Build desired data from default_plugins
# The following logic is similar to the original code but uses dicts/sets for comparisons.
@ -833,6 +834,7 @@ class Database:
suffix = 0
if hasattr(self, "suffix_rx") and self.suffix_rx.search(setting):
setting_id, suffix = setting.rsplit("_", 1)
suffix = int(suffix) # noqa: FURB123
if setting_id not in saved_settings:
self.logger.error(
f'{base_plugin.get("type", "core").title()} Plugin "{base_plugin["id"]}"\'s Template {template_id} has invalid setting "{setting}", skipping'
@ -840,19 +842,19 @@ class Database:
continue
# Check if belongs to a step
step_for_setting = None
step_id = None
for sid, step_set_list in steps_settings.items():
if setting in step_set_list:
step_for_setting = sid
step_id = sid
break
if not step_for_setting:
if not step_id:
self.logger.error(
f'{base_plugin.get("type", "core").title()} Plugin "{base_plugin["id"]}"\'s Template {template_id}`s setting "{setting}" doesn\'t belong to a step, skipping'
)
continue
desired_template_settings[(template_id, setting_id, suffix, step_for_setting)] = {"default": default, "order": order}
desired_template_settings[(template_id, setting_id, suffix, step_id, order)] = default
order += 1
# Template-level configs
@ -883,22 +885,21 @@ class Database:
config_name_clean = config_name.replace(".conf", "")
# Check if belongs to a step
step_for_config = None
step_id = None
for sid, step_conf_list in steps_configs.items():
if config in step_conf_list:
step_for_config = sid
step_id = sid
break
if not step_for_config:
if not step_id:
self.logger.error(
f'{base_plugin.get("type", "core").title()} Plugin "{base_plugin["id"]}"\'s Template {template_id}`s config "{config}" doesn\'t belong to a step, skipping'
)
continue
desired_template_configs[(template_id, config_type, config_name_clean, step_for_config)] = {
desired_template_configs[(template_id, config_type, config_name_clean, step_id, order)] = {
"data": content,
"checksum": checksum,
"order": order,
}
order += 1
@ -944,7 +945,7 @@ class Database:
to_delete.append({"type": "setting", "filter": {"plugin_id": sk[0], "id": sk[1]}})
# SELECTS
old_select_keys = set((s.setting_id, s.value, s.order) for s in old_selects.values())
old_select_keys = set(old_selects.keys())
# desired_selects is a set of (setting_id, value, order)
# We must correlate with known settings. If setting_id belongs to a plugin_id?
# Original code just handled removing selects not present. We'll trust that logic:
@ -1057,32 +1058,39 @@ class Database:
new_ts_keys = set(desired_template_settings.keys())
for tsk in new_ts_keys - old_ts_keys:
template_id, setting_id, suffix, step_id = tsk
default = desired_template_settings[tsk]["default"]
order = desired_template_settings[tsk]["order"]
to_put.append(Template_settings(template_id=template_id, setting_id=setting_id, suffix=suffix, step_id=step_id, default=default, order=order))
template_id, setting_id, suffix, step_id, order = tsk
default = desired_template_settings[tsk]
to_put.append(
Template_settings(
template_id=template_id,
setting_id=setting_id,
suffix=suffix,
step_id=step_id,
default=default,
order=order,
)
)
for tsk in old_ts_keys & new_ts_keys:
old_ts_val = old_template_settings[tsk]
new_default = desired_template_settings[tsk]["default"]
new_order = desired_template_settings[tsk]["order"]
if old_ts_val.default != new_default or old_ts_val.order != new_order:
template_id, setting_id, suffix, step_id = tsk
filter_data = {"template_id": template_id, "id": setting_id}
if step_id is not None:
filter_data["step_id"] = step_id
new_default = desired_template_settings[tsk]
if old_ts_val != new_default:
template_id, setting_id, suffix, step_id, order = tsk
filter_data = {"template_id": template_id, "setting_id": setting_id, "step_id": step_id}
if suffix is not None:
# Not all queries handle suffix well. If suffix is defined, add to filter:
filter_data["suffix"] = suffix
to_update.append(
{"type": "template_setting", "filter": filter_data, "data": {"default": new_default, "suffix": suffix, "order": new_order}}
{
"type": "template_setting",
"filter": filter_data,
"data": {"default": new_default, "suffix": suffix},
}
)
for tsk in old_ts_keys - new_ts_keys:
template_id, setting_id, suffix, step_id = tsk
filter_data = {"template_id": template_id, "id": setting_id}
if step_id is not None:
filter_data["step_id"] = step_id
template_id, setting_id, suffix, step_id, order = tsk
filter_data = {"template_id": template_id, "setting_id": setting_id, "step_id": step_id}
if suffix is not None:
filter_data["suffix"] = suffix
to_delete.append({"type": "template_setting", "filter": filter_data})
@ -1092,11 +1100,17 @@ class Database:
new_tc_keys = set(desired_template_configs.keys())
for tck in new_tc_keys - old_tc_keys:
template_id, ctype, cname, step_id = tck
template_id, ctype, cname, step_id, order = tck
conf_data = desired_template_configs[tck]
to_put.append(
Template_custom_configs(
template_id=template_id, type=ctype, name=cname, data=conf_data["data"], checksum=conf_data["checksum"], step_id=step_id
template_id=template_id,
type=ctype,
name=cname,
data=conf_data["data"],
checksum=conf_data["checksum"],
step_id=step_id,
order=order,
)
)
@ -1104,19 +1118,19 @@ class Database:
old_tc_obj = old_template_configs[tck]
new_tc_obj = desired_template_configs[tck]
if old_tc_obj.checksum != new_tc_obj["checksum"]:
template_id, ctype, cname, step_id = tck
filter_data = {"template_id": template_id, "name": cname, "type": ctype}
if step_id is not None:
filter_data["step_id"] = step_id
template_id, ctype, cname, step_id, order = tck
filter_data = {"template_id": template_id, "name": cname, "type": ctype, "step_id": step_id}
to_update.append(
{"type": "template_config", "filter": filter_data, "data": {"data": new_tc_obj["data"], "checksum": new_tc_obj["checksum"]}}
{
"type": "template_config",
"filter": filter_data,
"data": {"data": new_tc_obj["data"], "checksum": new_tc_obj["checksum"]},
}
)
for tck in old_tc_keys - new_tc_keys:
template_id, ctype, cname, step_id = tck
filter_data = {"template_id": template_id, "name": cname, "type": ctype}
if step_id is not None:
filter_data["step_id"] = step_id
template_id, ctype, cname, step_id, order = tck
filter_data = {"template_id": template_id, "name": cname, "type": ctype, "step_id": step_id}
to_delete.append({"type": "template_config", "filter": filter_data})
# APPLY CHANGES
@ -1178,6 +1192,7 @@ class Database:
session.add_all(to_put)
session.commit()
except SQLAlchemyError as e:
self.logger.debug(format_exc())
session.rollback()
return False, str(e)
@ -2723,6 +2738,8 @@ class Database:
order = 0
for setting, default in template.get("settings", {}).items():
setting_id, suffix = setting.rsplit("_", 1) if self.suffix_rx.search(setting) else (setting, None)
if suffix is not None:
suffix = int(suffix)
if setting_id in self.RESTRICTED_TEMPLATE_SETTINGS:
self.logger.error(
@ -3004,6 +3021,8 @@ class Database:
order = 0
for setting, default in template_data.get("settings", {}).items():
setting_id, suffix = setting.rsplit("_", 1) if self.suffix_rx.search(setting) else (setting, None)
if suffix is not None:
suffix = int(suffix)
if setting_id in self.RESTRICTED_TEMPLATE_SETTINGS:
self.logger.error(

View file

@ -62,11 +62,29 @@ def upgrade() -> None:
if "id" in [c["name"] for c in batch_op.impl.dialect.get_columns(op.get_bind(), "bw_ui_users")]:
batch_op.drop_column("id")
# Add the new order column to bw_template_settings
op.add_column("bw_template_settings", sa.Column("order", sa.Integer(), nullable=False))
# Add the new order column to bw_template_settings and set initial values
op.add_column("bw_template_settings", sa.Column("order", sa.Integer(), nullable=False, server_default="0"))
op.execute("SET @row_number = 0")
op.execute(
"""
UPDATE bw_template_settings
SET `order` = (@row_number:=@row_number + 1)
ORDER BY template_id, setting_id
"""
)
op.create_unique_constraint(None, "bw_template_settings", ["template_id", "setting_id", "order"])
# Add the new order column to bw_template_custom_configs
op.add_column("bw_template_custom_configs", sa.Column("order", sa.Integer(), nullable=False))
op.execute("SET @row_number = 0")
op.execute(
"""
UPDATE bw_template_settings
SET `order` = (@row_number:=@row_number + 1)
ORDER BY template_id, setting_id
"""
)
op.create_unique_constraint(None, "bw_template_custom_configs", ["template_id", "order"])
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc1' WHERE id = 1")

View file

@ -0,0 +1,42 @@
"""Upgrade to version 1.6.0-rc2
Revision ID: cb5750a9d1f7
Revises: 8a37fed772e9
Create Date: 2025-01-15 16:06:07.679683
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "cb5750a9d1f7"
down_revision: Union[str, None] = "8a37fed772e9"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column("bw_selects", sa.Column("order", sa.Integer(), nullable=False))
op.execute("SET @row_number = 0")
op.execute(
"""
UPDATE bw_selects
SET `order` = (@row_number:=@row_number + 1)
ORDER BY setting_id
"""
)
op.create_unique_constraint(None, "bw_selects", ["setting_id", "order"])
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc2' WHERE id = 1")
def downgrade() -> None:
op.drop_constraint(None, "bw_selects", type_="unique")
op.drop_column("bw_selects", "order")
# Revert the version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc1' WHERE id = 1")

View file

@ -0,0 +1,42 @@
"""Upgrade to version 1.6.0-rc2
Revision ID: 2f9f7600c78d
Revises: 6307fa627563
Create Date: 2025-01-15 16:18:37.077420
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "2f9f7600c78d"
down_revision: Union[str, None] = "6307fa627563"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column("bw_selects", sa.Column("order", sa.Integer(), nullable=False))
op.execute("SET @row_number = 0")
op.execute(
"""
UPDATE bw_selects
SET `order` = (@row_number:=@row_number + 1)
ORDER BY setting_id
"""
)
op.create_unique_constraint(None, "bw_selects", ["setting_id", "order"])
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc2' WHERE id = 1")
def downgrade() -> None:
op.drop_constraint(None, "bw_selects", type_="unique")
op.drop_column("bw_selects", "order")
# Revert the version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc1' WHERE id = 1")

View file

@ -62,11 +62,29 @@ def upgrade() -> None:
if "id" in [c["name"] for c in batch_op.impl.dialect.get_columns(op.get_bind(), "bw_ui_users")]:
batch_op.drop_column("id")
# Add the new order column to bw_template_settings
op.add_column("bw_template_settings", sa.Column("order", sa.Integer(), nullable=False))
# Add the new order column to bw_template_settings and set initial values
op.add_column("bw_template_settings", sa.Column("order", sa.Integer(), nullable=False, server_default="0"))
op.execute("SET @row_number = 0")
op.execute(
"""
UPDATE bw_template_settings
SET `order` = (@row_number:=@row_number + 1)
ORDER BY template_id, setting_id
"""
)
op.create_unique_constraint(None, "bw_template_settings", ["template_id", "setting_id", "order"])
# Add the new order column to bw_template_custom_configs
op.add_column("bw_template_custom_configs", sa.Column("order", sa.Integer(), nullable=False))
op.execute("SET @row_number = 0")
op.execute(
"""
UPDATE bw_template_settings
SET `order` = (@row_number:=@row_number + 1)
ORDER BY template_id, setting_id
"""
)
op.create_unique_constraint(None, "bw_template_custom_configs", ["template_id", "order"])
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc1' WHERE id = 1")

View file

@ -159,30 +159,56 @@ def upgrade() -> None:
# Copy data to new column
op.execute("UPDATE bw_instances SET status_new = status::text::instance_status_enum")
# Drop old column
batch_op.drop_column("status")
op.drop_column("bw_instances", "status")
# Rename new column to status
batch_op.alter_column("status_new", new_column_name="status", nullable=False)
op.alter_column("bw_instances", "status_new", new_column_name="status", nullable=False)
# Add the new order column to bw_template_settings
# Step 1: Add column as nullable
op.add_column("bw_template_settings", sa.Column("order", sa.Integer(), nullable=True))
# Step 2: Populate default values
op.execute('UPDATE bw_template_settings SET "order" = 0')
op.execute(
"""
UPDATE bw_template_settings
SET "order" = subquery.row_number
FROM (
SELECT id, ROW_NUMBER() OVER (ORDER BY template_id, setting_id) as row_number
FROM bw_template_settings
) as subquery
WHERE bw_template_settings.id = subquery.id
"""
)
# Step 3: Alter column to NOT NULL
op.alter_column("bw_template_settings", "order", nullable=False)
# Step 4: Add unique constraint
op.create_unique_constraint(None, "bw_template_settings", ["template_id", "setting_id", "order"])
# Add the new order column to bw_template_custom_configs
# Step 1: Add column as nullable
op.add_column("bw_template_custom_configs", sa.Column("order", sa.Integer(), nullable=True))
# Step 2: Populate default values
op.execute('UPDATE bw_template_custom_configs SET "order" = 0')
op.execute(
"""
UPDATE bw_template_custom_configs
SET "order" = subquery.row_number
FROM (
SELECT id, ROW_NUMBER() OVER (ORDER BY template_id) as row_number
FROM bw_template_custom_configs
) as subquery
WHERE bw_template_custom_configs.id = subquery.id
"""
)
# Step 3: Alter column to NOT NULL
op.alter_column("bw_template_custom_configs", "order", nullable=False)
# Step 4: Add unique constraint
op.create_unique_constraint(None, "bw_template_custom_configs", ["template_id", "order"])
# First drop Identity properties
op.execute(
"""

View file

@ -0,0 +1,60 @@
"""Upgrade to version 1.6.0-rc2
Revision ID: b56eb8d8dbf2
Revises: 940350925f36
Create Date: 2025-01-15 16:26:12.567104
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "b56eb8d8dbf2"
down_revision: Union[str, None] = "940350925f36"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add the new order column to bw_selects
# Step 1: Add column as nullable
op.add_column("bw_selects", sa.Column("order", sa.Integer(), nullable=True))
# Step 2: Populate default values
op.execute(
"""
UPDATE bw_selects
SET "order" = subquery.row_number
FROM (
SELECT value, ROW_NUMBER() OVER (ORDER BY setting_id) as row_number
FROM bw_selects
) as subquery
WHERE bw_selects.value = subquery.value
"""
)
# Step 3: Alter column to NOT NULL
op.alter_column("bw_selects", "order", nullable=False)
# Step 4: Add unique constraint
op.create_unique_constraint(None, "bw_selects", ["setting_id", "order"])
# Check if status_new exists before renaming
conn = op.get_bind()
inspector = sa.inspect(conn)
if "status_new" in [col["name"] for col in inspector.get_columns("bw_instances")]:
op.alter_column("bw_instances", "status_new", new_column_name="status")
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc2' WHERE id = 1")
def downgrade() -> None:
op.drop_constraint(None, "bw_selects", type_="unique")
op.drop_column("bw_selects", "order")
# Revert the version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc1' WHERE id = 1")

View file

@ -176,21 +176,73 @@ def upgrade() -> None:
op.drop_table("bw_instances")
op.rename_table("bw_instances_new", "bw_instances")
# Step 1: Add 'order' column as nullable with a default
with op.batch_alter_table("bw_template_settings") as batch_op:
batch_op.add_column(sa.Column("order", sa.Integer(), nullable=False, server_default="0"))
with op.batch_alter_table("bw_template_custom_configs") as batch_op:
batch_op.add_column(sa.Column("order", sa.Integer(), nullable=False, server_default="0"))
# Create new tables with order column and constraints
op.create_table(
"bw_template_settings_new",
sa.Column("id", sa.Integer, sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False),
sa.Column("setting_id", sa.String(256), nullable=False),
sa.Column("step_id", sa.Integer, nullable=False),
sa.Column("default", sa.TEXT, nullable=False),
sa.Column("suffix", sa.Integer, nullable=True, default=0),
sa.Column("order", sa.Integer, nullable=False, default=0),
sa.UniqueConstraint("template_id", "setting_id", "step_id", "suffix"),
sa.UniqueConstraint("template_id", "setting_id", "order"),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["setting_id"], ["bw_settings.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
# Step 2: Set default value for existing rows
op.execute("UPDATE bw_template_settings SET `order` = 0")
op.execute("UPDATE bw_template_custom_configs SET `order` = 0")
op.create_table(
"bw_template_custom_configs_new",
sa.Column("id", sa.Integer, sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False),
sa.Column("step_id", sa.Integer, nullable=False),
sa.Column(
"type",
sa.Enum(
"http",
"stream",
"server_http",
"server_stream",
"default_server_http",
"default_server_stream",
"modsec",
"modsec_crs",
"crs_plugins_before",
"crs_plugins_after",
name="custom_configs_types_enum",
),
nullable=False,
),
sa.Column("name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("checksum", sa.String(128), nullable=False),
sa.Column("order", sa.Integer, nullable=False, default=0),
sa.UniqueConstraint("template_id", "step_id", "type", "name"),
sa.UniqueConstraint("template_id", "order"),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
# Step 3: Alter 'order' column to NOT NULL
with op.batch_alter_table("bw_template_settings") as batch_op:
batch_op.alter_column("order", nullable=False)
with op.batch_alter_table("bw_template_custom_configs") as batch_op:
batch_op.alter_column("order", nullable=False)
# Copy data to new tables with default order=0
op.execute(
"""
INSERT INTO bw_template_settings_new (template_id, setting_id, step_id, "default", suffix, "order")
SELECT template_id, setting_id, step_id, "default", suffix, 0 FROM bw_template_settings
"""
)
op.execute(
"""
INSERT INTO bw_template_custom_configs_new (template_id, step_id, type, name, data, checksum, "order")
SELECT template_id, step_id, type, name, data, checksum, 0 FROM bw_template_custom_configs
"""
)
# Drop old tables and rename new ones
op.drop_table("bw_template_settings")
op.drop_table("bw_template_custom_configs")
op.rename_table("bw_template_settings_new", "bw_template_settings")
op.rename_table("bw_template_custom_configs_new", "bw_template_custom_configs")
# Re-enable foreign keys
op.execute("PRAGMA foreign_keys=ON;")

View file

@ -0,0 +1,48 @@
"""Upgrade to version 1.6.0-rc2
Revision ID: 92038d1e365e
Revises: 5b0ea031ccfc
Create Date: 2025-01-15 15:55:16.757661
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "92038d1e365e"
down_revision: Union[str, None] = "5b0ea031ccfc"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
with op.batch_alter_table("bw_selects") as batch_op:
batch_op.add_column(sa.Column("order", sa.Integer(), nullable=False, server_default="0"))
# Assign unique order values within each setting_id group
op.execute(
"""
UPDATE bw_selects SET "order" = (
SELECT COUNT(*) - 1
FROM bw_selects s2
WHERE s2.setting_id = bw_selects.setting_id
AND s2.rowid <= bw_selects.rowid
)
"""
)
with op.batch_alter_table("bw_selects") as batch_op:
batch_op.create_unique_constraint("uq_setting_order", ["setting_id", "order"])
# Update the version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc2' WHERE id = 1")
def downgrade() -> None:
with op.batch_alter_table("bw_selects") as batch_op:
batch_op.drop_constraint("uq_setting_order", type_="unique")
batch_op.drop_column("order")
# Revert the version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc1' WHERE id = 1")

View file

@ -87,6 +87,7 @@ class Settings(Base):
class Selects(Base):
__tablename__ = "bw_selects"
__table_args__ = (UniqueConstraint("setting_id", "order"),)
setting_id = Column(String(256), ForeignKey("bw_settings.id", onupdate="cascade", ondelete="cascade"), primary_key=True)
value = Column(String(256), primary_key=True)