Vlad

Schema guard

The declarative Rust based portion of the project discuss bellow.

Almost every website, from the simplest blogs to complex platforms, relies on databases. Of course, databases are no longer accessed directly from the code that generates the website, as was common in the past with PHP and JSP. Modern websites are typically structured in two or three layers:
Static or pseudo-static CRM content, which either doesn’t use a database or interacts with it as a third-party service.

Why Schema Guard Matters

The core principle of Schema Guard is ensuring that only verified and approved changes reach production (deployed by version number and tag).

Key aspects include:

Declarative (YAML Schema Definition) vs. Imperative (SQL):

Dynamic frontend, which directly interfaces with:
Backend or API, which handles direct database interactions for retrieving and persisting changes. Data includes everything from login credentials, login attempts, and user profile preferences to other dynamic information. The first step in development is creating the database, defining tables, and populating them with initial data. There are many ways to create database tables or entire schemas. Among the numerous approaches, several key methods stand out:
Using features of the chosen programming language and utilities to generate SQL scripts from classes or objects used for data access. This is convenient when supported by the language, but such support is more often absent than present.
Using ERP systems for graphical schema modeling and SQL generation. This is visually intuitive, but the generated scripts are limited by the database’s supported features. Not all database-specific capabilities can be generated, and manual script modifications prevent regeneration after schema changes.
Writing SQL scripts manually. This is the simplest and most common approach, but also the most time-intensive.
Using a declarative method, which generates the required SQL from a simple text-based format. This is the method we’ll explore next.

Getting Started

Let’s begin:

curl https://www.dbinvent.com/dist/rdbm-unix-latest.zip -o rdbm-latest.zip && unzip ./rdbm-latest.zip

This downloads a console application in binary format, ready to run on Linux or Windows (via WSL). The most convenient configuration method is creating and using a text file:

echo "db_host=localhost
db_name=postgres
db_port=5432
db_user=postgres
db_password=xxx" > db.cfg

If your database already contains tables, you can generate a schema snapshot:

./rdbm -c db.cfg --snapshot_to=schema.yaml snapshot

Next, you can edit or create a schema description file in YAML format, such as:

# databaseChangeLog
---
database:
- schema: # changeSet
  schemaName: public
  owner: postgres
  tables: # set of changes for tables
    - table: # will do a createTable or alter table
      tableName: test_samples
      description: Sample table comments
      columns:
      - column: # column definition
        name: id
        type: serial
        constraint: # changes on PK & NULL won’t apply
          primaryKey: true
          nullable: false
      - column:
        name: is_deleted
        type: BOOLEAN
        defaultValue: "false"
        constraint:
          nullable: false
      - column:
        name: text_value
        description: "my column comments"
        type: VARCHAR(256)

This text represents a highly readable and user-friendly YAML format, though it’s sensitive to indentation and formatting. Save the file as ./S1__schema.yaml. Then run:

./rdbm -c db.cfg --source_dir=. --dry_run=y migrate

This attempts to connect to the database and generates scripts to create the table without executing them. If the database doesn’t exist, the --createdb=y parameter will create it. Without --dry_run=y (or with --dry_run=n), the SQL executes, creating the table and logging the results in the public.rdbm_history history table. This table can be overridden, e.g., to deploy_history.deploys, using two parameters: --schema=deploy_history --table=deploys. Notably, re-running the command won’t alter the database, even if the history table is missing, but this applies only to scripts of this type. For detailed help, run:

./rdbm help migrate

Next, add to the script:

- column:
  name: created_on
  type: timestamp
  defaultValue: current_timestamp

Running the migration again adds the new column to the database. If the column is missing from an existing table, it’s added; if the table doesn’t exist, a new table is created with all specified columns.

Foreign Key Constraints

Relational database schemas often involve constraints. Here’s an example with an additional table:

   - table:
      tableName: test_ref
      columns:
      - column:
          name: id
          type: serial
          constraint:
            primaryKey: true
            nullable: false
      - column:
          name: sample
          type: INT
          constraint:
            nullable: false
            foreignKey:
              references: test_sample
              sql: DEFERABLE

The sql field is optional and defined in column_constraint per PostgreSQL [documentation](https://www.postgresql.org/docs/current/sql-createtable.html

(e.g., [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE referential_action ] [ ON UPDATE referential_action ] [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]).

Extended SQL at the Column Level Example:

- column:
  name: name
  type: VARCHAR(50)
  sql: UNIQUE

The SQL format aligns with column_constraint from PostgreSQL’s CREATE TABLE [documentation](https://www.postgresql.org/docs/current/sql-createtable.html, excluding NULL, PRIMARY KEY, and REFERENCES. Constraints and Extended SQL at the Table Level Similar to columns, you can define SQL fragments for table creation:

- table:
  tableName: users
  constraint: UNIQUE (iap, oauth_id)
  sql: PARTITION BY ...

Table constraint: Matches table_constraint in PostgreSQL’s CREATE TABLE [documentation](https://www.postgresql.org/docs/current/sql-createtable.html:

{ CHECK ( expression ) [ NO INHERIT ] | UNIQUE ( column_name [, ... ] ) index_parameters | EXCLUDE [ USING index_method ] ( ... ) index_parameters [ WHERE ( predicate ) ] }, 

excluding primary and foreign keys.
Table SQL: Follows formats like:

[ INHERITS ( parent_table [, ... ] ) ] [ PARTITION BY { RANGE | LIST | HASH } ( ... ) ] [ USING method ] [ WITH ( storage_parameter [= value] [, ... ] ) | WITHOUT OIDS ] [ ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP } ] [ TABLESPACE tablespace_name ].

Triggers The final table component is trigger definition, which requires a function (function creation is covered in a next article):

- table:
  tableName: emails   
  triggers:
    - trigger:
      name: ensure_lower_email_trg
      event: before update or insert
      when: for each row
      proc: make_lower_email() -- you have to have this function created

[!IMPORTANT] Current YAML Limitations

[!IMPORTANT] Schema Snapshot Restrictions

Part Two -