discourse/migrations/lib/database/schema.rb
Selase Krakani 99ace1be12
Some checks failed
Tests / core system (push) Has been cancelled
Tests / plugins system (push) Has been cancelled
Licenses / run (push) Has been cancelled
Linting / run (push) Has been cancelled
Migration Tests / Tests (push) Has been cancelled
Publish Assets / publish-assets (push) Has been cancelled
Tests / core backend (push) Has been cancelled
Tests / plugins backend (push) Has been cancelled
Tests / core frontend (Chrome) (push) Has been cancelled
Tests / plugins frontend (push) Has been cancelled
Tests / themes frontend (push) Has been cancelled
Tests / themes system (push) Has been cancelled
Tests / core frontend (Firefox ESR) (push) Has been cancelled
Tests / core frontend (Firefox Evergreen) (push) Has been cancelled
Tests / chat system (push) Has been cancelled
Tests / merge (push) Has been cancelled
DEV: Add support for table-level constraints (#34739)
Allow table-level check constraints in the Intermediate DB


In https://github.com/discourse/discourse/pull/34339, I’ve worked on
distinguishing between `user_custom_fields` tied to `user_fields` and
arbitrary `user_custom_fields` entries.

To support this, I’ve made both `field_id` and `name` nullable, which
requires a table-level constraint to ensure each entry has either a
`field_id` (referencing `user_fields`) or an arbitrary `name` for the
value.

This first pass supports only named  table-level `CHECK` constraints.

## Usage

```yaml
user_custom_fields:
      columns:
        exclude:
          - "id"
        modify:
          - name: "name"
            nullable: true
        add:
          - name: "field_id"
            datatype: numeric
          - name: "is_multiselect_field"
            datatype: boolean
      indexes:
      # ...
      constraints:
        - name: "require_field_id_or_name"
          condition: "field_id IS NOT NULL OR name IS NOT NULL"
        - name: "disallow_both_field_id_and_name"
          type: check      # default, only `check` supported for now
          condition: "NOT (field_id IS NOT NULL AND name IS NOT NULL)"
```



```sql
CREATE TABLE user_custom_fields
(
    created_at           DATETIME,
    field_id             NUMERIC,
    is_multiselect_field BOOLEAN,
    name                 TEXT,
    user_id              NUMERIC  NOT NULL,
    value                TEXT,
    CONSTRAINT require_field_id_or_name CHECK (field_id IS NOT NULL OR name IS NOT NULL),
    CONSTRAINT disallow_both_field_id_and_name CHECK (NOT (field_id IS NOT NULL AND name IS NOT NULL))
);
```
2025-09-12 20:14:51 +00:00

176 lines
2.6 KiB
Ruby

# frozen_string_literal: true
module Migrations::Database
module Schema
Table =
Data.define(:name, :columns, :indexes, :primary_key_column_names, :constraints) do
def sorted_columns
columns.sort_by { |c| [c.is_primary_key ? 0 : 1, c.name] }
end
end
Column = Data.define(:name, :datatype, :nullable, :max_length, :is_primary_key)
Index = Data.define(:name, :column_names, :unique, :condition)
Constraint = Data.define(:name, :type, :condition)
class ConfigError < StandardError
end
SQLITE_KEYWORDS = %w[
abort
action
add
after
all
alter
always
analyze
and
as
asc
attach
autoincrement
before
begin
between
by
cascade
case
cast
check
collate
column
commit
conflict
constraint
create
cross
current
current_date
current_time
current_timestamp
database
default
deferrable
deferred
delete
desc
detach
distinct
do
drop
each
else
end
escape
except
exclude
exclusive
exists
explain
fail
filter
first
following
for
foreign
from
full
generated
glob
group
groups
having
if
ignore
immediate
in
index
indexed
initially
inner
insert
instead
intersect
into
is
isnull
join
key
last
left
like
limit
match
materialized
natural
no
not
nothing
notnull
null
nulls
of
offset
on
or
order
others
outer
over
partition
plan
pragma
preceding
primary
query
raise
range
recursive
references
regexp
reindex
release
rename
replace
restrict
returning
right
rollback
row
rows
savepoint
select
set
table
temp
temporary
then
ties
to
transaction
trigger
unbounded
union
unique
update
using
vacuum
values
view
virtual
when
where
window
with
without
]
def self.escape_identifier(identifier)
if SQLITE_KEYWORDS.include?(identifier)
%Q("#{identifier}")
else
identifier
end
end
end
end