Sistema de base de datos de Euphoria (EDS)


Introducción

Mucha gente ha mostrado interés en acceder a bases de datos usando programas Euphoria. Algunos querían acceder a sistemas de gestión de base de datos de marca conocida desde Euphoria, otros querían una base de datos orientada a Euphoria simple y fácil de usar. EDS es lo más reciente. Provee un sistema de base de datos simple y extremadamente flexible, para usar con programas Euphoria.


Estructura de una base de datos EDS

En EDS, una base de datos es un archivo único con extensión .edb. Una base de datos EDS contiene 0 o más tablas. Cada tabla tiene un nombre, y contiene 0 o más registros. Cada registro consta de una parte de clave, y otra de datos. Laclave puede ser cualquier objeto de Euphoria - un átomo, una secuencia, secuencias anidadas, etc. Del mismo modo, los datos pueden ser cualquier objeto de Euphoria. No hay restricciones en el tamaño o estructura de la clave o los datos. Dentro de una tabla, las claves son únicas. Esto significa, no hay dos registros que tengan la misma clave.

Los registros de una tabla se almacenan en el orden ascendente del valor de la clave. Se utiliza una eficiente búsqueda binaria para referirse a los registros por clave. También se puede acceder a un registro directamente, sin búsqueda alguna, conociendo el número de registro dentro de la tabla. Los números de registro son enteros desde 1 al tamaño de la tabla (cantidad de registros). Incrementando el número de registro se pueden recorrer eficientemente todos los registros. Sin embargo tenga en cuenta que el número de registro puede cambiar toda vez que se inserte un nuevo registro, o cuando un registro se borra.

Las partes de datos y claves se almacenan en forma compacta, pero no se pierde exactitud al guardar o recuperar números de punto flotante o cualquier otro dato de Euphoria.

database.e trabajará sin cambios, en Windows, DOS, Linux o FreeBSD. El código se ejecuta casi el doble de rápido en Linux/FreeBSD que en DOS/Windows. Los archivos de la base de datos EDS se pueden copiar y compartir entre programas que corren en Linux/FreeBSD y DOS/Windows. Asegúrese de hacer una copia exacta byte por byte usando el modo de copiado "binario", en lugar del modo "texto" o "ASCII" porque podrían cambiar los terminadores de línea.

 Ejemplo:
    base de datos: "misdatos.edb"
          primera tabla: "accesos"
               registro #1:  clave: "García"   dato: "gordito"
               registro #2:  clave: "Pérez"    dato: "delgado"
        
          segunda tabla: "repuestos"
               registro #1:  clave: 134525    dato: {"martillo", 15.95, 500}
               registro #2:  clave: 134526    dato: {"sierra", 25.95, 100}
               registro #3:  clave: 134530    dato: {"destornillador", 5.50, 1500}
        
Este ejemplo es para interpretar el significado de la clave y de los datos. En consonancia con el espíritu de Euphoria, Ud. tiene flexibilidad total. A diferencia de otros sistemas de bases de datos, no se necesita que un registro EDS tenga un número fijo de campos, o campos con una longitud máxima predefinida.

En algunos casos no habrá ningún valor natural para la clave en los registros. En esos casos, se debería crear una clave entera sin significado alguno, pero única. Recordar que siempre se puede acceder a los datos por número de registro. Es fácil recorrer los registros buscando el valor de un campo en particular.


¿Cómo acceder a los datos?

Para reducir la cantidad de parámetros que se tienen que pasar, existe la noción de base de datos en uso, y tabla en uso. La mayoría de las rutinas usan este valor en uso (o actual) automáticamente. Normalmente se empieza por abrir (o crear) un archivo de base de datos, entonces se selecciona la tabla con la que se trabajará.

Usando db_find_key(), se puede mapear una clave a un número de registro. Este comando usa una eficiente búsqueda binaria. La mayoría de las otras rutinas a nivel de registro, esperan como argumento el número de registro. Dado el número de registro, se accede muy rápidamente a cualquier registro. También se puede acceder a todos los registros comenzando por el número 1 y recorriéndolos todos hasta el número de registro devuelto por db_table_size().


¿Cómo se recicla el almacenamiento?

Al borrar algo, como un registro, el espacio de ese ítem se marca como disponible en una lista áreas libres para uso posterior. Las áreas libres adyacentes se combinan en grandes áreas libres. Al necesitar más espacio y no teniéndolo disponible en la lista de áreas libres, el archivo crece en tamaño. Normalmente no hay un método automático para comprimir el tamaño del archivo, pero se puede usar db_compress() para reescribir completamente la base de datos, removiéndole los espacios sin uso.


Seguridad / Acceso multiusuario

Esta versión provee una forma sencilla de bloqueo de la base de datos para prevenir de accesos inseguros de parte de otros procesos.


Escalabilidad

Los punteros internos son de 4 bytes. En teoría, el tamaño máximo del archivo de una base de datos es 4 Gb. En la práctica, el límite es de 2 Gb debido a limitaciones en varias funciones de archivo de C usadas por Euphoria. Bajo pedido de los usuarios, las bases de datos EDS podrían expendirse en el futuro, más allá de los 2 Gb.

El algoritmo actual asigna 4 bytes de memoria por registro en la tabla actual. Por lo tanto, se necesitan como mínimo 4Mb de RAM por cada millón de registros en disco.

La búsqueda binaria por claves debería funcionar razonablemente bien con tablas grandes.

Las inserciones y borrados tardan levemente un poco más, según la tabla se haga más grande.

En el otro extremo de la escala, es posible crear bases de datos extremadamente pequeñas sin incurrir en mucho gasto de espacio en disco.


Renuncia

No almacenar datos importantes sin realizar una copia de seguridad. RDS no se hará responsable de cualquier daño o pérdida de información.


Rutinas de base de datos

En las descripciones posteriores, se usan los siguientes prefijos para indicar el tipo de objeto que se puede pasar o devolver:

x
- un objeto general (átomo o secuencia)
s
- una secuencia
a
- un átomo
i
- un entero
fn
- un entero usado como número de archivo
st
- una secuencia de cadena, o un átomo de caracter simple

db_create - crear una nueva base de datos
db_open - abrir una base de datos existente
db_select - seleccionar una base de datos para convertirla en la base de datos en uso
db_close - cerrar una base de datos
db_create_table - crear una nueva tabla en una base de datos
db_select_table - seleccionar una tabla para convertirla en la tabla en uso
db_delete_table - borrar una tabla
db_table_list - obtener un listado de todas las tablas de la base de datos
db_table_size - obtener la cantidad de registros de la tabla en uso
db_find_key - encontrar rápidamente el registro con un cierto valor de clave
db_record_key - obtener la clave de un registro
db_record_data - obtener los datos de un registro
db_insert - insertar un nuevo registro en la tabla en uso
db_delete_record - borrar un registro de la tabla en uso
db_replace_data - reemplazar los datos de un registro
db_compress - comprimir una base de datos
db_dump - volcar el contenido de una base de datos
db_fatal_id - manejar errores fatales de la base de datos



db_create

Sintaxis: include database.e
i1 = db_create(s, i2)
Descripción:
Crea una nueva base de datos. Una nueva base de datos se creará en el archivo con ruta dada por 's'. 'i2' indica el tipo de bloqueo que se aplicará al archivo creado. 'i1' es un código de error que indica éxito o fracaso. Los valores de 'i2' pueden ser tanto DB_LOCK_NO (sin bloqueo) o DB_LOCK_EXCLUSIVE (bloqueo exclusivo). Si 'i1' es DB_OK, significa que la base de datos se creó exitosamente. Esta base de datos se convierte en la base de datos en uso a la que se aplicarán las demás operaciones de base de datos.
Comentarios:
Si la ruta 's' no termina en .edb, esta extensión se agregará automáticamente.

Si la base de datos ya existe, no se sobreescribirá. db_create() devolverá DB_EXISTS_ALREADY.

El número de versión se almacena en el archivo de base de datos. De esta forma, las futuras versiones del software de la base de datos podrán reconocer el formato, leerlo y operar con él de alguna manera.

Ejemplo:
if db_create("misdatos", DB_LOCK_NO) != DB_OK then
    puts(2, "No se puede crear la base de datos!\n")
    abort(1)
end if
Ver también: db_open, db_close

db_open

Sintaxis: include database.e
i1 = db_open(s, i2)
Descripción:
Abre una base de datos Euphoria existente. El archivo que contiene la base de datos está dado por 's'. 'i1' es un código de retorno que indica el éxito o fracaso de la operación. 'i2' indica el tipo de bloqueo que quiere aplicar a la base de datos mientras la abre. Esta base de datos se convierte en la base de datos en uso a la que se aplicarán las demás operaciones de base de datos.

Los códigos de retorno son:

    global constant DB_OK = 0   -- éxito
             DB_OPEN_FAIL = -1  -- no se puede abrir el archivo
             DB_LOCK_FAIL = -3  -- no se puede bloquear el archivo de la
                                --    forma pedida
Comentarios:
Los tipos de bloqueo que se pueden usar son: DB_LOCK_NO (sin bloqueo), DB_LOCK_SHARED (bloqueo compartido para acceso de lectura solamente) y DB_LOCK_EXCLUSIVE (para acceso de lectura/escritura). DB_LOCK_SHARED está soportado solamente por Linux/FreeBSD. Permite leer la base de datos, sin escribir absolutamente nada en ella. Si se solicita DB_LOCK_SHARED en WIN32 o DOS32, será tratado como si el pedido se hubiera efectuado con DB_LOCK_EXCLUSIVE.

Si el bloqueo falla, su programa debería esperar unos segundos antes de reintentarlo. Otro proceso podría estar accediendo a la base de datos en ese momento.

Típicamente, los programas DOS mostrarán un mensaje de "error crítico" si intentan acceder a una base de datos que está bloqueada.

Ejemplo:
tries = 0
while 1 do
    err = db_open("misdatos", DB_LOCK_SHARED) 
    if err = DB_OK then
        exit
    elsif err = DB_LOCK_FAIL then
        tries += 1
        if tries > 10 then
            puts(2, "demasiados intentos, lo siento\n")
            abort(1)
        else    
            sleep(5)
        end if
    else
        puts(2,"No se puede abrir la base de datos!\n")
        abort(1)
    end if
end while

Ver también: db_create, db_close

db_select

Sintaxis: include database.e
i = db_select(s)
Descripción:
Selecciona una nueva base de datos ya abierta, para convertirla en la base de datos en uso. Las operaciones subsecuentes de base de datos se aplicarán a esta base. 's' es la ruta del archivo de base de datos que originalmente se abrió con db_open() o db_create(). 'i' es un código de retorno que indica el éxito de la operación (DB_OK) o su fracaso.
Comentarios:
Al crear (db_create()) o abrir (db_open()) una base de datos, ésta se convierte en la base de datos en uso. Use db_select() cuando quiera cambiar entre bases de datos abiertas, por ejemplo, para copiar registros de una a otra.

Después de seleccionar una nueva base de datos, se debería seleccionar una tabla de esa base, usando db_select_table().

Ejemplo:
if db_select("empleados") != DB_OK then
    puts(2, "No se puede seleccionar la base de datos de empleados\n")
end if
Ver también: db_open

db_close

Sintaxis: include database.e
db_close()
Descripción: Desbloquea y cierra la base de datos en uso.
Comentarios:
Llamar a este procedimiento cuando se haya terminado de usar la base de datos en uso. Se removerá cualquier bloqueo, permitiendo a otros procesos acceder al archivo de base de datos.
Ver también: db_open

db_create_table

Sintaxis: include database.e
i = db_create_table(s)
Descripción:
Crea una tabla nueva dentro de la base de datos en uso. El nombre de la tabla está dado por la secuencia de caracteres 's' y no puede ser igual al de ninguna otra tabla existente en la base de datos en uso.
Comentarios: La tabla que se crea tiene inicialmente 0 registros y además, se convierte en la tabla en uso.
Ejemplo:
if db_create_table("mi_nueva_tabla") != DB_OK then
    puts(2, "No se puede crear mi_nueva_tabla!\n")
end if
Ver también: db_delete_table

db_select_table

Sintaxis:
include database.e
i = db_select_table(s)
Descripción:
La tabla cuyo nombre está dado por 's', se convierte en la tabla en uso. El código de retorno 'i' será DB_OK si la tabla existe en la base de datos en uso, de lo contrario obtendrá DB_OPEN_FAIL.
Comentarios:
All record-level database operations apply automatically to the tabla en uso.
Ejemplo:
if db_select_table("salario") != DB_OK then
    puts(2, "No se puede hallar la tabla salario!\n")
    abort(1)
end if
Ver también: db_create_table, db_delete_table

db_delete_table

Sintaxis:
include database.e
delete_table(s)
Descripción:
Borrar una tabla de la base de datos en uso. El nombre de la tabla está dado por 's'.
Comentarios:
Se borran todos los registros y el espacio ocupado por la tabla se libera. Si la tabla es la tabla en uso, ésta queda indefinida.

Si no existe una tabla con el nombre dado por 's', no ocurre nada.

Ver también: db_create_table, db_select_table

db_table_list

Sintaxis:
s = db_table_list()
Descripción:
Devuelve una secuencia con todos los nombres de tablas de la base de datos en uso. Cada elemento de 's' es una secuencia de caracteres que contiene el nombre de una tabla.
Ejemplo:
sequence names

names = db_table_list()
for i = 1 to length(names) do
    puts(1, names[i] & '\n')
end for
Ver también: db_create_table

db_table_size

Sintaxis: include database.e
i = db_table_size()
Descripción: Devuelve la cantidad de registros de la tabla en uso.
Ejemplo:
-- recorre todos los registros de la tabla en uso
for i = 1 to db_table_size() do
    if db_record_key(i) = 0 then
        puts(1, "No se encontró la clave\n")
        exit
    end if
end for
Ver también: db_select_table

db_find_key

Sintaxis: include database.e
i = db_find_key(x)
Descripción:
Busca el registro de la tabla en uso con el valor de clave 'x'. Si se encuentra, se devuelve el número de registro. Si no se encuentra, se devuelve el número de registro que debería tener la clave, como número negativo.
Comentarios:
Se usa una búsqueda binaria para encontrar la clave en la tabla en uso. La cantidad de comparaciones es proporcional al logaritmo de la cantidad de registros en la tabla.

Se puede seleccionar un rango de registros, buscando los valores primero y último de la clave. Si esos valores de la clave no existen, al menos se obtiene un valor negativo que muestra donde debería estar si existiesen. Suponer que se quiere saber cuales registros tienen claves mayores que "GGG" y menores que "MMM". Si se obtiene -5 para la clave "GGG", significa que un registro de clave "GGG" debería insertarse como registro número 5. -27 para "MMM" significa que un registro de clave "MMM" debería insertarse como registro número 27. Esto indica rápidamente que todos los registros , >= 5 y < 27 cumplen la condición.

Ejemplo:
rec_num = db_find_key("Millennium")
if rec_num > 0 then
    ? db_record_key(rec_num)
    ? db_record_data(rec_num)
else
    puts(2, "No se encuentra, pero si lo insertara,\n")
    puts(2, "su número de registro sería #%d\n", -rec_num)
end if
Ver también: db_record_key, db_record_data, db_insert

db_record_key

Sintaxis: include database.e
x = db_record_key(i)
Descripción: Devuelve la parte de clave del número de registro indicado por 'i' de la tabla en uso.
Comentarios:
Cada registro en una base de datos Euphoria consta de una parte de clave y otra de datos, pudiendo ser cada uno de ellos cualquier átomo o secuencia de Euphoria.
Ejemplo:
puts(1, "La clave del registro Nº 6 es: ")
? db_record_key(6)
Ver también: db_record_data

db_record_data

Sintaxis: include database.e
x = db_record_data(i)
Descripción: Devuelve la parte de datos del número de registro indicado por 'i' de la tabla en uso.
Comentarios:
Cada registro en una base de datos Euphoria consta de una parte de clave y otra de datos, pudiendo ser cada uno de ellos cualquier átomo o secuencia de Euphoria.
Ejemplo:
puts(1, "El dato del registro Nº 6 es: ")
? db_record_data(6)
Ver también: db_record_key

db_insert

Sintaxis: include database.e
i = db_insert(x1, x2)
Descripción:
Inserta un nuevo registro en la tabla en uso. La clave del registro es 'x1' y el dato del registro es 'x2'. Tanto 'x1' como 'x2' pueden ser cualquier objeto de datos de Euphoria, átomos o secuencias. El código de retorno 'i1' será DB_OK si el registro se inserta.
Comentarios:
Dentro de una tabla, todas las claves tienen que ser únicas. Si ya existe un registro con ese mismo valor de clave, db_insert() fallará obteniendo DB_EXISTS_ALREADY.
Ejemplo:
if db_insert("Pérez", {"Pedro", 100, 34.5}) != DB_OK then
    puts(2, "Falló la inserción del registro!\n")
end if
Ver también: db_find_key, db_record_key, db_record_data

db_delete_record

Sintaxis: include database.e
db_delete_record(i)
Descripción: Borra el número de registro indicado por 'i' de la tabla en uso.
Comentarios: El número de registro 'i' tiene que ser un entero entre 1 y la cantidad de registros de la tabla en uso.
Ejemplo:
db_delete_record(55)
Ver también: db_insert, db_table_size

db_replace_data

Sintaxis: include database.e
db_replace_data(i, x)
Descripción: Reemplaza los datos de número de registro 'i' en la tabla en uso, por 'x'. 'x' puede ser cualquier secuencia o átomo de Euphoria.
Comentarios: El número de registro 'i' tiene que estar entre 1 y la cantidad de registros de la tabla en uso.
Ejemplo:
db_replace(67, {"Pedro", 150, 34.5})
Ver también: db_delete_record

db_compress

Sintaxis: include database.e
i = db_compress()
Descripción:
Comprime la base de datos en uso. La base de datos en uso se copia a un nuevo archivo, de forma tal que los bloques de espacio sin usar se eliminan. Si no ocurre ningún error, 'i' se establece a DB_OK, y el nuevo archivo de base de datos mantendrá el mismo nombre. Por seguridad, el archivo original descomprimido se renombrará con la extensión .t0 (o .t1, .t2 ,..., .t99). Si la compresión no tiene éxito, la base de datos permancerá sin cambios y la copia de seguridad no se realizará.
Comentarios:
Al borrar ítems de una base de datos, se crean bloques de espacio vacío dentro del archivo. El sistema los tiene en cuenta, intentando usarlos como nuevo espacio de almacenamiento cada vez que se realiza una inserción de datos. db_compress() copiará la base de datos en uso sin incluir esas áreas libres. El tamaño del archivo de base de datos, por ende, disminuirá.

Si los archivos de copia de seguridad alcanzan la extensión .t99, se deberán borrar alguno de ellos.

Ejemplo:
if db_compress() != DB_OK then
    puts(2, "Falló la compresión!\n")
end if
Ver también: db_create

db_dump

Sintaxis: include database.e
db_dump(fn, i)
Descripción:
Vuelca los contenidos de una base de datos Euphoria ya abierta. Los contenidos se descargan en un archivo o dispositivo 'fn'. Se muestran todos los registros de todas las tablas. Si 'i' no es cero, entonces también se muestra el volcado byte a byte de bajo nivel. El volcado de bajo nivel solamente será significativo para quienes conozcan el formato interno de una base de datos Euphoria.
Ejemplo:
if db_open("misdatos", DB_LOCK_SHARED) != DB_OK then
    puts(2, "No se puede abrir la base de datos!\n")
    abort(1)
end if
fn = open("db.txt", "w")
db_dump(fn, 0)

Ver también: db_open

db_fatal_id

Sintaxis: include database.e
db_fatal_id = i
Descripción:
Se pueden capturar ciertos errores fatales de base de datos, instalando un gestor de errores. Simplemente se sobreescribe la variable global db_fatal_id con un identificador de una rutina creada por el programador. El procedimiento tiene que tomar un argumento único, como una secuencia. Al ocurrir ciertos errores, se llamará a este procedimiento con un mensaje de error como argumento. Este procedimiento debería terminar con una llamada a abort().
Ejemplo:
procedure mi_error_fatal(sequence msg)
    puts(2, "Ocurrió un error fatal - " & msg & '\n')
    abort(1)
end procedure

db_fatal_id = routine_id("mi_error_fatal")
Ver también: db_close