# Devkron Data Model (DDM)

Dialecto y herramientas para diseñar y generar estructuras de bases de datos relacionales

---

Repositorio en github: <https://github.com/Induxsoft/dkl-ddm>

---

## Contexto
Al definir modelos de bases de datos relacionales generalmente se emplean dos clases de herramientas:

* CASE con una pesada interfaz de usuario y que requiere mucho trabajo para detallar a través de diagramas, adecuadamente los objetos y poder generar a partir de ellos el SQL DDL o bien,
* directamente SQL DDL o una aplicación de DBA con interfaz de usuario que lo genere con la desventaja del fuerte acoplamiento al motor de bases de datos.

## Necesidad
Una manera simple de definir estructuras relacionales a través de archivos de texto (código fuente) que no requiera herramientas con interfaz de usuario, 
que sea independiente del motor de bases de datos, que pueda generar el SQL DDL y que no tenga una sintaxis tan ofuscada como el SQL, ni laboriosa como 
el XML o el JSON.

## Solución
Aprovechando las características de definición de sintaxis de Devkron Language, se definió un dialecto declarativo que expresa en nuestra opinión, con claridad y simplicidad los 
objetos y sus relaciones, además de adicionar características como la herencia de abstracciones.

DDM sirve para dos cosas distintas, que conviene no confundir:

* **Crear estructuras desde cero:** a partir de un archivo `.ddm`, `dbgen` produce el script SQL completo (MySQL o SQL Server).
* **Actualizar una base de datos existente, incluso en producción:** `install-model` compara el modelo nuevo con el último modelo instalado y aplica solo los cambios que son seguros, sin eliminar datos. Esta modalidad solo soporta MySQL y sus reglas se describen en [Reglas al actualizar una base existente](#reglas-al-actualizar-una-base-existente).

## Dialecto
DDM se basa en los siguientes conceptos:

### Modelo
Es un conjunto de abstracciones y entidades

### Abstracción
Es un modelo de tabla que no se implementará físicamente pero que incluye definiciones de columnas o índices que podrán heredar las entidades descendientes.

### Entidad
Es la definición de una tabla física e incluye columnas e índices

```
#include "ddm.dkh" // Los patrones de sintaxis de DDM

model "nombre del modelo"
{
  abstract "nombre_de_la_abstracción"
  {
    @"nombre_de_una_columna" tipo_de_datos atributos...
    ...
  }
  
  entity "nombre_de_la_entidad" : abstracción_de_la_abstracción_de_la_que_desciende // (opcional)
  {
    ... definiciones de columnas ...
  }
}
```

Una entidad puede descender de 0 o hasta 5 abstracciones.

#### Ejemplo
```
#include "ddm.dkh"
model "CRM Sales"
{
    abstract "induxsoftTableModel"
    {
        @"sys_pk" int key autoincrement
        @"sys_guid" string(32) unique required
        @"sys_dtcreated" datetime required
        @"sys_timestamp" datetime required
        @"sys_recver" int
        @"sys_deleted" bool
        @"sys_lock" int
        @"sys_info" string(32)
        @"sys_user" string(32)
    }

    abstract "induxsoftEnumModel"
    {
        @"id" int key 
        @"const" string(150)
    }

    entity "tabla_solita" //Una tabla de que no desciende de ninguna abstracción
    {
      @"campo1" string(10) key 
      @"campo2" int
    }
    
    entity "tuser" : "induxsoftTableModel"
    {
        @"userid" string required unique
        @"username" string(250) required
    }

    entity "domicilio" : "induxsoftTableModel"
    {
        @"codPos" string 
    }
    entity "agente" : "induxsoftTableModel"
    {
        @"codigo" string(15) required unique
        @"nombre" string(150) required unique
        @"uf_iuser" int ref:"tuser" ["sys_pk"] unique

        @"codnomina" string(50)
        @"email" string(50)
        @"notas" string(2048)
        @"pcomision" decimal(18,8)
        @"telefono" string(150)
        @"domicilio1" int ref:"domicilio" ["sys_pk"]
        @"uf_sys_baja" bool
        
        index "indice1" unique
        {
            field "uf_iuser"
            field "nombre" desc
        }
    }

    entity "cmdedocivil" : "induxsoftEnumModel" { }
    entity "cmdsexo" : "induxsoftEnumModel" { }

    entity "ctpersona" : "induxsoftTableModel"
    {
        @"descripcion" string(50)
    }
    entity "ciudad" : "induxsoftTableModel"
    {
        @"codigo" string(8)
    }
    entity "pais" : "induxsoftTableModel"
    {
        @"codigo" string(8)
    }
    entity "persona" : "induxsoftTableModel"
    {
        @"nombre" string(150) required
        // @"telefono" string(15)
        @"email" string(100)

        @"apellido1" string(50) required
        @"apellido2" string(50)
        @"autoid" string(150)
        @"codigo" string(32)
        @"codpostal" string(10)
        @"colonia" string(100)
        @"domicilio" string(255)
        @"email2" string(100)
        @"fnacimiento" date 
        @"msn" string(100)
        @"notas" memo
        @"skype" string(100)
        @"sys_guid_contacto" string(32)
        @"tel_casa" string(80)
        @"tel_movil" string(80)
        @"tel_trabajo" string(80)
        @"tratamiento" string(10)
        @"tel_movil" string(80)
        @"edocivil" int ref:"cmdedocivil" ["id"] 
        @"sexo" int ref:"cmdsexo" ["id"]
        @"icategoria" int ref:"ctpersona" ["sys_pk"] required
        @"iciudad" int ref:"ciudad" ["sys_pk"]
        @"iejecutivo" int ref:"agente" ["sys_pk"]
        @"nacionalidad" int ref:"pais" ["sys_pk"]
    }

    entity "ctorganizacion" : "induxsoftTableModel"
    {
        @"descripcion" string(50)
    }
    entity "organizacion" : "induxsoftTableModel"
    {
        @"nombre" string(150) unique required

        @"codigo" string(32)
        @"codpostal" string(10)
        @"domicilio" string(255)
        @"email" string(100)
        @"fax" string(80)
        @"nomcomercial" string(150)
        @"notas" memo
        @"sys_guid_cliente" string(32)
        @"sys_guid_otro" string(32)
        @"sys_guid_proveedor" string(32)
        @"tel1" string(80)
        @"tel2" string(80)
        @"tel3" string(80)
        @"website" string(150)

        @"icategoria" int ref:"ctorganizacion" ["sys_pk"] required
        @"iciudad" int ref:"ciudad" ["sys_pk"]
        @"iejecutivo" int ref:"agente" ["sys_pk"]
        @"nacionalidad" int ref:"pais" ["sys_pk"]
    }

    entity "sales_pipeline_stage" : "induxsoftTableModel"
    {
        @"ref_pipeline" ref:"sales_pipeline" required
        @"sequence" int required    //El número de secuencia de las etapas en el pipeline
        @"name" string(50) required
        @"probability" decimal(4,1)
        @"stuck_in_days" int
    }

    entity "sales_pipeline" : "induxsoftTableModel"
    {
        @"name" string(50)
        @"probability" bool
    }
}
```

Observaciones sobre el ejemplo:

* `tabla_solita` no desciende de ninguna abstracción y define su propia clave (`campo1`).
* `cmdedocivil` y `cmdsexo` heredan de una abstracción y no agregan nada propio (`{ }`).
* `sales_pipeline_stage` hace referencia a `sales_pipeline`, que se define después: el orden de las entidades no importa.
* `persona` declara `tel_movil` dos veces. Cuando una columna se declara dos veces en la misma entidad, **prevalece la última definición**; no es un error.
* Los comentarios se escriben con `//`.

#### Reglas de herencia y composición de modelos

* **Qué se hereda:** columnas **e índices** de cada abstracción ascendente.
* **Varios ascendentes:** se aplican en orden, del primero al quinto; si dos definen una columna con el mismo nombre, gana el último. Las columnas propias de la entidad prevalecen sobre las heredadas, de modo que una entidad puede redefinir una columna heredada (por ejemplo, para cambiar su tamaño).
* **Las abstracciones no heredan de otras abstracciones:** el patrón `abstract` no admite ascendente (solo `entity` lo admite).
* **Posición de las abstracciones:** una abstracción puede declararse en cualquier parte del modelo; todas se procesan antes que las entidades.
* **Ascendente inexistente:** produce el error `El ascendente 'nombre' no está definido`.
* **Modelos que incluyen otros modelos:** dentro del bloque `model` se puede usar `#include "otro_modelo.ddm"`. Las abstracciones y entidades del modelo incluido pasan a formar parte del modelo principal. El repositorio lo usa así: `dbm.sys_tables.ddm` incluye `induxsoft.abstracts.ddm`.
* **Abstracciones incluidas en el repositorio:** `induxsoft.abstracts.ddm` define `TableModel` (`sys_pk` autoincremental como clave, `sys_guid` único, `sys_timestamp`, `sys_dtcreated`, `sys_deleted`, entre otras columnas de control) y `EnumModel` (`id` y `const`).
* **Nombre del modelo:** el nombre que se da a `model` es el que usa `dbgen` para crear la base de datos en el script que genera (ver [dbgen](#dbgen)).

### Alternar verificación de compatibilidad

* wide compatibility (predeterminado)
* strict compatibility

Estas directivas controlan cómo se comporta `install-model` cuando un modelo nuevo se compara con el ya instalado. Se escriben dentro del bloque `model`, solas en su línea:

```
model "mi_modelo"
{
    wide compatibility          // es el valor predeterminado

    entity "a" { ... }          // modo wide

    strict compatibility

    entity "b" { ... }          // modo strict
    entity "c" { ... }          // modo strict
}
```

**Alcance.** El modo es **posicional**: cada entidad toma el modo de la última directiva que aparece antes de ella. Si no hay ninguna directiva, todas las entidades son `wide`. Las abstracciones no se ven afectadas. La variante por entidad (`strict compatibility: 'entidad'`) está prevista en `ddm.dkh` pero deshabilitada; no funciona.

**Qué cambia cada modo.** Los dos modos aplican las mismas reglas de [Reglas al actualizar una base existente](#reglas-al-actualizar-una-base-existente) con una sola diferencia:

| Situación | `wide` | `strict` |
|---|---|---|
| Una columna `required` o `key` de una entidad existente **ya no aparece** en el modelo nuevo | Se acepta; la columna permanece en la base de datos | **Incompatibilidad**: se aborta la instalación |

En ninguno de los dos modos se elimina la columna de la base de datos. El modo `strict` sirve para avisar que una columna `NOT NULL` que se quitó del modelo seguirá existiendo con esa restricción y que, si la aplicación deja de llenarla, los `INSERT` empezarán a fallar. Si se trabaja con un modelo `.json` (resultado de la generación de primer paso) que no incluye el campo `compatibility` por tabla, se aplica el comportamiento `wide`.

### Definición de columnas
Las columnas se definen dentro del cuerpo de las entidades o abstracciones.

La sintaxis es: ```@"nombre_columna" tipo atributo1 atributo2 atributo3```

tipo es alguna de los siguientes:

* bool 
* int 
* datetime
* date
* time
* decimal(precisión, escala)
* string(tamaño)
* memo
* mmemo
* lmemo

Los atributos (son opcionales y) pueden ser los siguientes:

* unique - Valores de clave única
* required - No se admiten nulos 
* autoincrement - El valor se incrementa automáticamente por parte del motor
* key - Es el campo de clave primaria

Las claves foráneas se indican con la sintaxis:

Sintaxis simplificada de definición de referencia que dejará al generador la inferencia del tipo y campo clave de la tabla referida.
```@"nombre_columna" ref: "entidad_referenciada"```

Sintaxis completa en donde explícitamente se indica el tipo de la columna y el nombre del campo clave referido
```@"nombre_columna" tipo ref: "entidad_referenciada" [campo_clave_referido]```

#### Equivalencia de tipos en MySQL

| DDM | MySQL |
|---|---|
| `bool` | `BOOL` |
| `int` | `INT` |
| `datetime` | `DATETIME` |
| `date` | `DATE` |
| `time` | `TIME` |
| `decimal(p,s)` | `DECIMAL(p,s)` |
| `string(n)` | `VARCHAR(n)`; si `n` es mayor que 4096, `TEXT` |
| `memo` | `TEXT` |
| `mmemo` | `MEDIUMTEXT` |
| `lmemo` | `LONGTEXT` |

* **Alias de tipos aceptados:** `str` y `varchar` equivalen a `string`; `boolean` y `bit` a `bool`; `integer` a `int`; `text` a `memo`.
* **Otros tipos** (por ejemplo `bigint`, `smallint`, `float`, `double`, `blob`) se pasan tal cual al SQL. El generador no los valida y, al actualizar una base, la compatibilidad solo conoce los pares de tipos de la [tabla de tipos compatibles](#cambios-de-tipo-tamaño-y-precisión).
* **`string` sin tamaño** equivale a `string(32)` (el ejemplo `@"userid" string required unique` crea un `VARCHAR(32)`).
* **`decimal`:** indique siempre `(precisión, escala)`. Los valores por defecto previstos (12 y 4) no se aplican por un defecto del código (ver [Limitaciones conocidas](#limitaciones-conocidas)).

#### Reglas de los atributos

* `key` **implica `required`**: una clave primaria nunca admite nulos. No agrega `unique` porque la clave primaria ya es única. `primarykey` es sinónimo de `key`.
* Se pueden combinar hasta **tres** atributos en una columna.
* Una columna sin `required` admite nulos.
* `autoincrement` requiere que la columna sea la clave de la tabla (regla de MySQL).
* `unique` en una columna genera la restricción `UNIQUE` en la misma definición de la columna; el nombre del índice lo asigna MySQL.
* **Clave primaria compuesta:** no puede declararse con `key` en dos columnas, porque cada una emitiría su propio `PRIMARY KEY`. Use un `index ... unique` con ambas columnas.
* **Comparación de nombres:** al comparar modelos, los nombres de columna no distinguen mayúsculas de minúsculas.
* **Sin efecto, no usar:**
  * Los atributos `indexed` e `indexed_desc` se leen pero ningún generador los aplica.
  * Las sintaxis `list:` y `enum:` y los bloques `enum` están definidos en `ddm.dkh` pero el generador no los procesa.

#### Detalles de las claves foráneas

* **Sintaxis simplificada:** la entidad referenciada debe existir en el modelo y tener una columna `key`. Si no la tiene, la generación falla con el mensaje `No se puede resolver o la columna referenciada de la tabla 'X' desde la tabla 'Y' porque no tiene una clave primaria indicada.`
* **Sintaxis completa:** la columna indicada entre corchetes debe existir en la entidad referenciada (`La columna 'K' referenciada de la tabla 'X' desde la tabla 'Y' no existe.`). Si se omite el tipo, se toma el de la columna referida, con su tamaño, precisión y escala.
* **Entidad inexistente:** `La tabla 'X' a la que se hace referencia en 'Y' no existe en el modelo.`
* **Orden de definición:** no importa. Las llaves foráneas se crean después de todas las tablas, con `ALTER TABLE ... ADD FOREIGN KEY`.
* **Alcance:** solo se soportan referencias de **una columna**, sin acciones `ON DELETE` ni `ON UPDATE`; MySQL asigna el nombre de la restricción.
* **Nulabilidad:** una columna `ref:` admite nulos salvo que lleve `required`.
* **Al actualizar:** una columna `ref:` agregada a una tabla que ya existe se crea **sin** su llave foránea (ver [Qué no hace nunca una actualización](#qué-no-hace-nunca-una-actualización)).

### Definición de índices
Los índices se definen dentro del cuerpo de las entidades o abstracciones

```
  index "indice1" unique //modificador opcional unique para indicar que es un índice de clave única
  {
    field "campo1"
    field "campo2" desc // Modificador de orden del campo del índice
  }
```

#### Detalles de los índices

* De los tipos de índice solo `unique` tiene efecto. El parámetro `algorithm` que admite la sintaxis se guarda en el JSON pero el generador MySQL no lo usa.
* Los índices se **heredan** de las abstracciones igual que las columnas.
* Orden de creación en una instalación: primero todas las tablas, luego los índices y por último las llaves foráneas.
* **Al actualizar**, los índices de entidades que ya existen no se comparan: agregar, quitar o cambiar un `index` en una entidad existente no produce ninguna acción (ver [Qué no hace nunca una actualización](#qué-no-hace-nunca-una-actualización)). Los índices de las tablas nuevas sí se crean.

## Instalación de las herramientas de DDM
Simplemente coloque en la carpeta de binarios de Devkron los archivos de la carpeta src de este repositorio.

Los archivos incluidos son:

* ddm.dkh - Las definiciones de sintaxis
* dbgen.dkl - El generador de código de primer paso
* gen_mysql.dkl - Generador de segundo paso para MySQL
* gen_sqlserver.dkl - Generador de segundo paso para SQLServer
* install-model.dkl - Instalador de modelos (DDM)

Además de los anteriores, las herramientas necesitan estos archivos de la misma carpeta `src`, por lo que debe copiarla **completa**:

* ddm.json.dkl - Generación de primer paso (modelo a JSON) y comparación de modelos
* ddm.mysql.dkl - Generación de SQL para MySQL, incluido el SQL de actualización
* ddm.sqlserver.dkl - Generación de SQL para SQL Server (lo usa gen_sqlserver.dkl)
* dbm.dkl - Lógica de instalación de modelos y de la bitácora (lo usan install-model.dkl y dbgen.dkl)
* dbm.sys_tables.ddm - Modelo de las tablas de bitácora que crea install-model
* induxsoft.abstracts.ddm - Abstracciones `TableModel` y `EnumModel`
* dba.dkl - Utilidad de administración de conexiones

## Uso de las herramientas

### Instalación de modelos (install-model.dkl)

```
dkl install-model db="mi_conexión" model="mi_modelo"
```
* `db`: Cadena con el nombre cualificado de una conexión existente.
* `model`: Es la ruta y nombre del archivo (modelo) `.ddm` o `.json` que representa la estructura de una base de datos.

Ejemplo de línea de comando para instalar un modelo ddm
```
dkl install-model "db=mibasededatos@migrupodeapps" "model=mimodelonuevo.ddm"
```

Para la instalación del nuevo modelo la herramienta trabaja con 2 tablas sys_model_log y sys_model_ddl_log que serán creadas en la base de datos de la cadena de conexión si no existen.

Cuando el proceso de instalación ha finalizado podra ver en:

* sys_model_log: información del modelo instalado.
* sys_model_ddl_log: información de las acciones ejecutadas (nuevas tablas, nuevas columnas, modificación de columnas, etc.)

Nota: la herramienta usa además una tercera tabla, `sys_packs`, que también se crea si no existe (ver [Cómo trabaja install-model](#cómo-trabaja-install-model)).

Campos incompatibles en true
* required
* primarykey

Esa lista resume la regla principal de incompatibilidad: una **columna nueva** que lleva `required` o `key` (`primarykey`) no puede agregarse a una tabla existente. Las reglas completas están en [Reglas al actualizar una base existente](#reglas-al-actualizar-una-base-existente).

También es posible instalar un modelo `.json` (el resultado de la generación de primer paso, por ejemplo `dkl dbgen src="modelo.ddm" out="modelo.json"`).

#### Funciones

```
dkl install-model "db=mi_conexion" "action=funcion"
```
* `db`: Cadena con el nombre cualificado de una conexión existente.
* `action`: Una de las siguientes funciones.
    - infer-model: deducir el modelo en base a una aplicación (db).
    - show-model: obtener el modelo de una aplicación (db).
    - show-last-error: obtener el último error de instalación ocurrido.

Ejemplo de linea de comando para obtener el modelo de una aplicación.
```
dkl install-model "db=mibasededatos@migrupodeapps" "action=show-model"
```

Detalles de cada función:

* **infer-model** lee la estructura real de la base de datos y la **registra como el modelo vigente** (inserta una fila nueva en `sys_model_log`). Como la comparación de `install-model` siempre usa el último modelo registrado, esta función es la forma de **re-sincronizar** el modelo registrado después de un cambio hecho a mano en la base de datos. Reemplaza toda la línea base, por lo que las diferencias de inferencia descritas en [Cómo se infiere un modelo](#cómo-se-infiere-un-modelo) deben revisarse antes de la siguiente instalación.
* **show-model** imprime el modelo vigente en JSON. Si todavía no existen las tablas de bitácora, primero infiere el modelo y la salida incluye mensajes antes del JSON.
* **show-last-error** muestra el mensaje del último fallo registrado **al ejecutar sentencias DDL** (lee `sys_model_ddl_log`). No muestra las incompatibilidades: tras una incompatibilidad responde `No se encontraron errores.`, porque ese error se detecta antes de ejecutar DDL y solo se imprime en la consola.
* Un `action` mal escrito no produce un error propio: se interpreta como una instalación y falla con `No existe el archivo de origen o no se ha indicado el parámetro (model).`

#### Cómo trabaja install-model

* **Motor soportado:** solo MySQL. Con otro motor termina con `El motor de base de datos de la conexión no esta soportada.`
* **Bitácora:** además de `sys_model_log` y `sys_model_ddl_log`, la herramienta usa una tercera tabla, `sys_packs` (versión de esquema por paquete). Las tres se crean si no existen.
  * `sys_model_log`: una fila por ejecución. `source` (nombre del archivo del modelo), `install` (modelo que se pidió instalar, en JSON), `analisys` (listas de tablas comunes y nuevas), `compatible`, `success` y `model` (el modelo vigente al terminar; es el que se compara en la siguiente ejecución).
  * `sys_model_ddl_log`: una fila por sentencia DDL. `ref_model_log`, `action` (`create_table`, `create_index`, `create_reference`, `new_column`, `change_column`), `cmdsql` (el SQL ejecutado), `jsondef` (la definición en JSON), `success` y `message` (el error, si lo hubo).
  * `sys_packs`: `pack`, `schema_ver` y `sys_timestamp`.
* **Primera ejecución sobre una base existente:** si faltan las tablas de bitácora, se crean y el modelo de la base de datos real se **infiere** y se registra como línea base. A continuación el modelo nuevo se compara contra esa línea base.
* **Orden de ejecución:** (1) tablas nuevas; (2) índices de las tablas nuevas; (3) llaves foráneas de las tablas nuevas; (4) columnas nuevas y modificadas de las tablas existentes. Cada sentencia se registra en `sys_model_ddl_log` y el modelo vigente se actualiza paso a paso.
* **Fallos a mitad de la instalación:** MySQL confirma cada sentencia DDL de forma implícita, por lo que no existe una transacción que deshaga lo ya aplicado. Si una sentencia falla, el estado parcial queda registrado en `sys_model_log.model` y una nueva ejecución continúa desde ahí sin repetir lo aplicado.
* **Tablas del sistema:** `sys_model_log` y `sys_model_ddl_log` se excluyen siempre del análisis.

#### Reglas al actualizar una base existente

Cuando ya existe un modelo instalado, `install-model` no ejecuta el modelo completo: lo **compara** con el último modelo registrado (no con la base de datos real) y aplica solo la diferencia. Un cambio hecho a mano en la base de datos no se detecta hasta que se ejecuta `infer-model`.

Estas reglas parten de un principio: **en producción nunca se eliminan datos ni se reduce lo que ya existe.**

##### Reglas por columna

Se aplican a las tablas que existen en ambos modelos. Los nombres de columna se comparan sin distinguir mayúsculas.

| Situación | Resultado |
|---|---|
| Columna nueva sin `required` ni `key` | **Compatible**: `ALTER TABLE ... ADD` (`unique` se aplica) |
| Columna nueva con `required` o `key` | **Incompatible** en ambos modos. DDM no tiene valor por defecto, así que no hay forma de crearla sobre filas existentes |
| Columna existente que sale del modelo | No se hace nada y la columna permanece. Si era `required` o `key`: incompatible solo en `strict` |
| Misma definición (tipo, tamaño, precisión, escala, `required`, `unique`, `key`, `autoincrement`) | Nada que hacer |
| Mismo tipo y tamaño, pero cambia `required`, `unique`, `key` o `autoincrement` | **Incompatible** en ambos modos y en cualquier dirección, incluso quitar `required` |
| Cambia el tipo, el tamaño, la precisión o la escala | `MODIFY COLUMN` si existe un tipo compatible; si no, **incompatible** |

Consecuencias prácticas:

* Una columna ya creada **no puede pasar de admitir nulos a `required`** (ni al revés) mediante `install-model`. La receta está en [Agregar una columna obligatoria a una tabla con datos](#agregar-una-columna-obligatoria-a-una-tabla-con-datos).
* Una columna obligatoria nueva solo puede crearse sin `required` y endurecerse después.
* Si cambian varios atributos a la vez, el mensaje de incompatibilidad nombra solo uno (prevalece `key`, luego `required`, luego `unique`). Si lo único que cambia es `autoincrement`, la acción es incompatible y el mensaje queda vacío.

##### Cambios de tipo, tamaño y precisión

Cuando cambia el tipo, el tamaño, la precisión o la escala de una columna, el tipo resultante es siempre el **más amplio** de los dos, según esta tabla (simétrica):

| Par de tipos | Resultado |
|---|---|
| `char` + `varchar` | `varchar` |
| `char` + `string` | `string` |
| `string` + `memo` / `mmemo` / `lmemo` | `memo` / `mmemo` / `lmemo` |
| `memo` + `mmemo`, `mmemo` + `lmemo`, `memo` + `lmemo` | el mayor de los dos |
| `bit` + `tinyint` | `tinyint` |
| `bit` + `bool` | `bool` |
| `bool` + `int`, `tinyint` + `int`, `int` + `smallint` | `int` |
| `tinyint` + `smallint`, `bool` + `smallint` | `smallint` |
| `int` + `float` / `double` / `decimal` | `float` / `double` / `decimal` |
| `float` + `decimal`, `double` + `decimal` | `decimal` |
| `float` + `double` | `double` |
| `date` + `datetime` | `datetime` |

* Cualquier otro par es **incompatible** (por ejemplo `string` y `int`, `string` y `decimal`, `int` y `datetime`, `bigint` y `int`) con el mensaje `No se encontro una definición para los tipos de datos.`
* **Solo se amplía.** En `string` el tamaño resultante es el mayor de los dos; en `decimal`, la mayor precisión y la mayor escala (si además cambia el tipo, la precisión pasa a `11` más la escala). Pedir un tamaño menor no falla, pero se conserva el mayor.
* Con un tamaño mayor a 4096, el `string` pasa a `TEXT` en MySQL.

##### Cómo se comprueba la compatibilidad antes de ejecutar

1. Se compara el modelo nuevo con el modelo vigente.
2. Si **cualquier** acción es incompatible, cada una se imprime en la consola como JSON y la instalación **se aborta completa** con el error 99 (`No es posible instalar el modelo, se encontraron incompatibilidades.`).
3. Esa comprobación ocurre **antes** de ejecutar sentencias DDL: no se crea ni se modifica nada, ni siquiera lo que sí era compatible.

El motivo de una incompatibilidad solo aparece en la consola; para ejecuciones desatendidas, capture la salida estándar. Para saber de antemano si un modelo se instalaría, use el ensayo descrito en [Ensayo previo de un cambio](#ensayo-previo-de-un-cambio).

##### Qué no hace nunca una actualización

* Eliminar tablas o columnas, o reducir tipos y tamaños.
* Comparar o modificar los **índices** de tablas que ya existen. Se crean los de las tablas nuevas.
* Crear la **llave foránea** de una columna `ref:` agregada a una tabla existente.
* Cambiar valores por defecto, restricciones `CHECK`, motor, juego de caracteres o intercalación: DDM no tiene sintaxis para ellos y se usan los valores del servidor.
* Detectar cambios hechos a mano en la base de datos (solo se compara contra el último modelo registrado).

Todo esto debe hacerse con SQL manual. Si el cambio altera algo que DDM compara (por ejemplo la nulabilidad de una columna), ejecute después `infer-model` para re-sincronizar el modelo registrado.

##### Cómo se infiere un modelo

`infer-model`, `show-model` (cuando no hay bitácora) y la primera instalación sobre una base existente deducen el modelo de la base real, con estas equivalencias:

| En MySQL | En el modelo inferido |
|---|---|
| `varchar(n)` | `string` de tamaño `n` |
| `text`, `mediumtext`, `longtext` | `memo`, `mmemo`, `lmemo` |
| `int` | `int` |
| `tinyint`, `bit` | `bool` |
| `decimal(p,s)` | `decimal` con su precisión y escala |
| cualquier otro tipo (`bigint`, `char`, `timestamp`, ...) | el nombre original del tipo, con su tamaño o precisión |

* `required` se deduce de `IS_NULLABLE`, `unique` de las columnas marcadas `UNI`, `key` de `PRI` y `autoincrement` de `auto_increment`.
* Como los tipos que no están en la tabla se conservan con su nombre real, una columna `bigint` en la base contra `int` en el `.ddm` se compara como **incompatible**, y una `char` contra `string` genera un `MODIFY` a `VARCHAR`. Las columnas del `.ddm` deben coincidir también en nulabilidad y unicidad con las de la base.
* Los índices de más de una columna se deducen como índices del modelo.

#### Mensajes de error habituales

| Mensaje | Causa |
|---|---|
| `No se ha indicado una cadena de conexión (db).` | Falta el parámetro `db` |
| `No se encontro el grupo de conexiones X` / `No se encontro la conexión X` | El nombre cualificado (`conexion@grupo`) no existe en el repositorio de conexiones |
| `El motor de base de datos de la conexión no esta soportada.` | `install-model` solo soporta MySQL |
| `No existe el archivo de origen o no se ha indicado el parámetro (model).` | El archivo no existe, falta `model`, o el `action` está mal escrito |
| `El tipo de archivo no esta soportado.` | `model` debe ser `.ddm` o `.json` |
| `El ascendente 'X' no está definido` | Una entidad hereda de una abstracción que no existe |
| `La tabla 'X' a la que se hace referencia en 'Y' no existe en el modelo.` | Una referencia apunta a una entidad inexistente |
| `No hay objetos de bases de datos definidos` | El archivo no define ningún modelo |
| `No es posible instalar el modelo, se encontraron incompatibilidades.` | Error 99: ver [Cómo se comprueba la compatibilidad antes de ejecutar](#cómo-se-comprueba-la-compatibilidad-antes-de-ejecutar) |

---
Acerca del proceso de generación

1. EL modelo es validado sintácticamente de acuerdo a los patrones establecidos en ddm.dkh
2. El modelo es transformado a JSON (primer paso de generación)
3. El modelo descrito en JSON produce SQL o cualquier otra salida (segundo paso de generación)

El primer paso es independiente del motor de bases de datos; el segundo es el que da la sintaxis de MySQL o de SQL Server (`gen_mysql.dkl`, `gen_sqlserver.dkl`).

# Utilidades

## dba

Utilidad de línea de comandos para administrar conexiones y crear bases de datos.

```
./dkl dba [opción] [parámetros]
```
Opciones:

* ```show all``` Muestra todas las conexiones en el repositorio
* ```show``` Muestra la información de la conexión indicada en el parámetro ```qn```
* ```delete``` Elimina la conexión indicada en el parámetro ```qn```
* ```create database``` Crea una base de datos y su registro de conexión con la información de los parámetros ```qn``` y ```settings```
* ```create connection``` Crea un registro de conexión con la información de los parámetros ```qn``` y ```settings```

Parámetros
* ```qn``` Indica un nombre cualificado de conexión (nombreconexion@grupodeaplicacion)
* ```settings``` Indica un conjunto de valores para establecer una conexión a la base de datos, los pares clave valor deben delimitarse por comas y a su vez, cada clave y su valor se delimitan por dos puntos, para conocer las claves (parámetros de configuración conocidos) vea [Funciones de administración de conexiones](https://docs.induxsoft.net/es/devkron/Bibliotecas-de-funciones/dbr/dbr.md)

Ejemplos
```
# Mostrar todas las conexiones
./dkl dba show all

# Mostrar información de una conexión
./dkl dba show qn=miconexion@grupoapps

# Eliminar una conexión
./dkl dba delete qn=miconexion@grupoapps

# Crear una conexión a una base de datos existente
# Observe cómo está formado el parámetro settings para indicar los datos de la conexión
./dkl dba create connection qn=miconexion@grupoapps "settings=Engine:MY_SQL,Host:nombreoipdelhost,Port:3306,UserName:root, Password:12345,SslMode:0,DefaultSchema:nombrebasededatos"

# Crear una base de datos y su registro de conexión
./dkl dba create database qn=miconexion@grupoapps "settings=Engine:MY_SQL,Host:nombreoipdelhost,Port:3306,UserName:root, Password:12345,SslMode:0,DefaultSchema:nombrebasededatos"
```

## dbgen

Utilidad de línea de comandos para generar estructuras de datos.

`dbgen` tiene tres modos de operación, que se eligen según los parámetros que se indiquen:

| Modo | Parámetros que lo activan | Resultado |
|---|---|---|
| Generar desde un modelo | `src` | Script SQL o JSON de primer paso |
| Comparar dos modelos | `a` o `b` | Script SQL de actualización |
| Generar desde una conexión | `db` | Modelo DDM o JSON de una base existente |

Si no se indica ninguno, termina con `Debe indicar un origen (db, src, a, b).`

### Generación de estructura desde DDM

Genera la estructura sql o json a partir de un archivo Devkron Data Model (DDM) y lo guarda en la ruta especificada.

```powershell
dkl dbgen src="archivo_fuente" fmt="programa_generador" out="archivo_salida"

```

- `src`: es la ruta y el nombre de un archivo en dialecto DDM
- `fmt` (opcional): es el nombre del programa generador de segundo paso, si este parámetro se omite, se producirá como salida un objeto JSON que representa la estructura como resultado de la generación de primer paso.
- `out`: es el archivo en donde se escribirán los resultados de la generación.

Ejemplo de línea de comando para generar código MySQL
```powershell
dkl dbgen src="modelo.ddm" fmt="gen_mysql.dkl" out="script.sql"
```

Ejemplo de línea de comando para generar código SQLServer
```powershell
dkl dbgen src="modelo.ddm" fmt="gen_sqlserver.dkl" out="script.sql"
```

Ejemplo de línea de comando para obtener el JSON de primer paso (por ejemplo, para comparar modelos o instalarlo con `install-model`)
```powershell
dkl dbgen src="modelo.ddm" out="modelo.json"
```

Además de lo anterior:

* **Valores de `fmt`:** además del nombre del programa generador (`gen_mysql.dkl`, `gen_sqlserver.dkl`) se aceptan las formas cortas `mysql`, `gen_mysql`, `sqlserver`, `gen_sqlserver` y `json`.
* **Archivo JSON junto al script:** cuando `fmt` genera SQL, `dbgen` escribe además, en la misma carpeta y con el mismo nombre base que `out`, un archivo `.json` con el resultado del primer paso (con `out="script.sql"` se crea `script.json`). Sirve como entrada `a` o `b` en una comparación posterior.
* **La base de datos se crea con el script:** el script de MySQL comienza con `CREATE DATABASE IF NOT EXISTS <nombre>; USE <nombre>;`, donde `<nombre>` es el nombre del modelo en minúsculas, con los espacios, puntos, dos puntos, puntos y coma, guiones y barras sustituidos por `_` (el modelo `"CRM Sales"` produce `crm_sales`). El de SQL Server hace lo equivalente con `IF NOT EXISTS`. Quite o ajuste esas líneas si va a aplicar el script sobre una base ya existente.
* **Orden del script:** tablas, luego índices y por último llaves foráneas.
* La carpeta de `out` debe existir.

### Comparación de dos modelos y generación de actualización

```powershell
dkl dbgen a="archivo_fuente_A" b="archivo_fuente_B" c="lista,de,tablas,a,filtrar" fmt="programa_generador" out="archivo_salida"
```

- `a`: es la ruta y el nombre de un archivo json que representa la estructura como resultado de la generación de primer paso.
- `b`: es la ruta y el nombre de un archivo json que representa la estructura como resultado de la generación de primer paso.
- `c` (opcional): es una lista de nombres de tablas separadas por coma(,) las cuales serán comparadas.
- `fmt` (opcional): es el nombre del programa generador de segundo paso, si este parámetro se omite, se producirá como salida un objeto JSON que representa la estructura como resultado de la generación de primer paso.
- `out`: es el archivo en donde se escribirán los resultados de la generación.

Ejemplo de línea de comando para comparar 2 esquemas y generar nuevo código MySQL
```powershell
dkl dbgen a="esquema_a.json" b="esquema_b.json" fmt="gen_mysql.dkl" out="script.sql"
```

Ejemplo de línea de comando para comparar 2 esquemas y generar nuevo código SQLServer
```powershell
dkl dbgen a="esquema_a.json" b="esquema_b.json" fmt="gen_sqlserver.dkl" out="script.sql"
```

Además de lo anterior:

* `a` es el modelo de partida y `b` el modelo al que se quiere llegar. El script contiene las tablas nuevas (con sus índices y llaves foráneas) y las columnas nuevas o modificadas, con las mismas reglas de [Reglas al actualizar una base existente](#reglas-al-actualizar-una-base-existente).
* **Las incompatibilidades no abortan:** `dbgen` las imprime en la consola y las omite del script, que solo contiene lo compatible. `install-model`, en cambio, aborta la instalación completa. Por eso una incompatibilidad impresa por `dbgen` significa que `install-model` no instalaría ese modelo.
* **Filtro `c`:** las tablas nuevas se incluyen siempre. Si ningún nombre de `c` coincide con una tabla común a ambos modelos, se comparan **todas** las tablas.
* El script de comparación no incluye `CREATE DATABASE` ni `USE`, porque el resultado de una comparación no lleva nombre de modelo.

### Generar estructura desde una conexión

Genera la estructura DDM o JSON a partir de una conexión (qname) y lo guarda en la ruta especificada.

```powershell
dkl dbgen db="qname" fmt="ddm/json" out="archivo_salida" schema="base_de_datos"
```

- `db`: Cadena con el nombre cualificado de una conexión existente. **Requerido**
- `fmt`: Formato de salida (ddm/json). **Opcional**, si se omite se asume "ddm".
- `out`: Ruta y nombre del archivo donde se escribirá el resultado de la generación. **Requerido**
- `schema`: Nombre de la base de datos del que se desea obtener su estructura. **Opcional**, si se omite se asume la base de datos configurada en la cadena de conexión (qname).

Ejemplo de línea de comando para generar la estructura DDM a partir de una conexión.
```powershell
dkl dbgen db="miconexion@grupoapps" fmt="ddm" out="ruta/archivo.ddm"
```

Ejemplo de línea de comando para generar la estructura JSON a partir de una conexión.
```powershell
dkl dbgen db="miconexion@grupoapps" fmt="json" out="ruta/archivo.json"
```

Además de lo anterior:

* El `.ddm` generado es un **punto de partida**, no un modelo final: solo reconstruye los índices de más de una columna (los índices de una sola columna que no son únicos no aparecen) y expresa las llaves foráneas en forma simplificada (`ref:"tabla"`, con la clave referida deducida). Revíselo antes de usarlo como fuente.
* **Tipos en el `.ddm` generado:** se toman del tipo de columna de MySQL. Solo `varchar`, `text`, `mediumtext` y `longtext` se traducen a `string`, `memo`, `mmemo` y `lmemo`; los demás (por ejemplo `tinyint(1)` o `bigint`) aparecen con su nombre de MySQL y deben ajustarse al vocabulario de DDM (`bool`, ...). Con `fmt="json"` se usan las equivalencias de [Cómo se infiere un modelo](#cómo-se-infiere-un-modelo).

# Operación en producción

Esta sección reúne los procedimientos recomendados para trabajar con `install-model` sobre bases de datos en producción. Se derivan de las reglas anteriores; pruebe cada procedimiento en una copia de la base antes de aplicarlo.

## Adoptar DDM sobre una base existente

1. Genere el punto de partida: `dkl dbgen db="miconexion@grupoapps" fmt="ddm" out="modelo.ddm"`.
2. Revíselo y complételo: índices de una sola columna, llaves foráneas cuyo campo clave no sea la clave primaria, atributos de columna.
3. Haga el [ensayo previo](#ensayo-previo-de-un-cambio) **antes** de la primera instalación. Esa primera instalación infiere la base real como línea base y cualquier diferencia de tipo (`bigint`, `char`, ...), de nulabilidad o de unicidad contra el `.ddm` aparece como `MODIFY COLUMN` o como incompatibilidad.
4. Si en esa base nunca se usó `install-model`, verifique que no existan solo `sys_model_log` y `sys_model_ddl_log` sin `sys_packs`: la herramienta exige las tres tablas y, si falta alguna, intenta crearlas todas y fallaría por tabla existente. En ese caso cree `sys_packs` a mano.

## Ensayo previo de un cambio

```powershell
# 1. JSON del modelo vigente (si es la primera vez, quite del archivo los mensajes previos al JSON)
dkl install-model "db=miconexion@grupoapps" "action=show-model" > a.json

# 2. JSON del modelo nuevo (la carpeta de salida debe existir)
dkl dbgen src="nuevo.ddm" out="b.json"

# 3. Script con el cambio
dkl dbgen a="a.json" b="b.json" fmt="gen_mysql.dkl" out="cambio.sql"
```

Revise `cambio.sql` y la consola: cualquier incompatibilidad impresa abortaría `install-model`. Pruebe el script en una copia antes de instalar.

## Agregar una columna obligatoria a una tabla con datos

DDM no puede crearla ya requerida ni endurecer después una columna existente. Procedimiento:

1. Agregue la columna **sin** `required` en el `.ddm` e instale.
2. Rellene los datos existentes.
3. Aplique a mano `ALTER TABLE ... MODIFY COLUMN ... NOT NULL` con la definición completa de la columna.
4. Ejecute `dkl install-model "db=..." "action=infer-model"` para que el modelo registrado refleje la base.
5. Marque la columna como `required` en el `.ddm` y repita el ensayo previo hasta que no queden diferencias.

El paso 4 reemplaza toda la línea base por lo inferido de la base real; cualquier otra diferencia entre la inferencia y el `.ddm` (tipos como `bigint` o `char`, atributo `unique`) saldrá en la siguiente instalación.

## Ampliar el tamaño de una columna obligatoria

Es compatible, pero el `MODIFY COLUMN` generado puede dejar la columna sin `NOT NULL` (ver [Limitaciones conocidas](#limitaciones-conocidas)). Después de instalar, vuelva a aplicar `NOT NULL` a mano y verifique `IS_NULLABLE` en `information_schema.COLUMNS`.

## Retirar una columna

Quitarla del `.ddm` no la elimina de la base de datos. Si es obligatoria, la aplicación debe seguir llenándola o hacerla nullable a mano. Use `strict compatibility` en esa entidad para que DDM avise antes de que se retire del modelo. Para eliminarla hay que usar SQL manual y luego `infer-model`.

## Renombrar una columna o una tabla

DDM no tiene renombrado: un nombre nuevo se interpreta como una **columna nueva** (o tabla nueva) y la anterior permanece. Renombre con SQL manual y re-sincronice con `infer-model`.

## Cambiar índices o llaves foráneas de tablas existentes

Se hace con SQL manual; después, `infer-model` si desea que el modelo registrado lo refleje. Los índices y llaves foráneas de las tablas nuevas sí los crea DDM.

## Después de un fallo

1. Lea la consola: el motivo de una incompatibilidad solo aparece ahí.
2. Revise `sys_model_ddl_log` (filas con `success = 0`) o ejecute `show-last-error`.
3. Si falló un índice o una llave foránea de una tabla nueva, créelo a mano: una nueva ejecución no lo reintentará (ver [Limitaciones conocidas](#limitaciones-conocidas)).
4. Corrija la causa y vuelva a ejecutar; lo ya aplicado no se repite.

# Límites de DDM

DDM no tiene sintaxis para:

* Valores por defecto de columna (`DEFAULT`).
* Restricciones `CHECK`.
* Motor de almacenamiento, juego de caracteres ni intercalación (se usan los del servidor).
* Acciones `ON DELETE` / `ON UPDATE` en llaves foráneas ni llaves foráneas de varias columnas.
* Clave primaria compuesta (use un índice único).
* Renombrar o eliminar tablas, columnas, índices o llaves foráneas.
* Vistas, procedimientos, disparadores u otros objetos que no sean tablas.

# Limitaciones conocidas

Corresponden a la versión revisada del repositorio (rama `main`, commit `2770168`, 26 de mayo de 2026). Los puntos 1 y 3 describen el comportamiento esperado de MySQL a partir del SQL que genera la herramienta; verifíquelos en una copia de su base.

1. **Ampliar una columna quita `NOT NULL` y `AUTO_INCREMENT`.** El `MODIFY COLUMN` que se genera para una acción de cambio no incluye esos atributos y MySQL reemplaza la definición completa de la columna. `UNIQUE` y `PRIMARY KEY` se conservan, porque son índices independientes. El modelo registrado no registra la pérdida, de modo que no se detecta después. Aplique a mano `NOT NULL` tras la instalación y verifique `IS_NULLABLE`. Aplica también a las acciones de cambio que no modifican nada real (por ejemplo, un tamaño menor en el modelo nuevo, o `char` contra `string` en una línea base inferida).
2. **Un fallo no cambia necesariamente el código de salida** (el resultado de la instalación se descarta y el error solo se imprime) y el detalle de una incompatibilidad solo va a la consola; `show-last-error` no lo muestra. En ejecuciones desatendidas, capture y revise la salida estándar.
3. **Un índice o una llave foránea de una tabla nueva que falla** queda registrado como instalado y una nueva ejecución no lo reintenta, porque los índices y llaves foráneas de tablas existentes no se comparan. Revise `sys_model_ddl_log` y cree a mano lo que falte.
4. **`decimal` sin `(precisión, escala)`** no recibe los valores por defecto previstos. Declare siempre ambos.
5. **El campo `compatible` de `sys_model_log`** se toma solo de la primera tabla comparada, no del conjunto.

# Resumen rápido

| Quiero... | ¿Con DDM? |
|---|---|
| Crear tablas, índices y llaves foráneas nuevas | Sí |
| Agregar una columna opcional a una tabla existente | Sí |
| Agregar una columna `required` o `key` a una tabla existente | No directamente: [procedimiento](#agregar-una-columna-obligatoria-a-una-tabla-con-datos) |
| Ampliar un `string` o un `decimal`, o pasar a un tipo más amplio | Sí (ver [Ampliar el tamaño de una columna obligatoria](#ampliar-el-tamaño-de-una-columna-obligatoria)) |
| Reducir tamaños o cambiar a un tipo incompatible | No |
| Cambiar `required`, `unique`, `key` o `autoincrement` de una columna existente | No (manual) |
| Eliminar o renombrar columnas y tablas | No (manual) |
| Cambiar índices o llaves foráneas de tablas existentes | No (manual) |
| Valores por defecto, `CHECK`, motor, juego de caracteres, acciones de llave foránea, clave primaria compuesta | No hay sintaxis |
| Saber si un modelo se instalaría sin aplicarlo | [Ensayo previo](#ensayo-previo-de-un-cambio) con `dbgen a/b` |
| Volver a sincronizar el modelo registrado tras un cambio manual | `install-model ... action=infer-model` |