Compare commits

..

No commits in common. "main" and "v0.1.1" have entirely different histories.
main ... v0.1.1

52 changed files with 620 additions and 6827 deletions

View file

@ -1,24 +0,0 @@
on:
push:
tags:
- "*"
jobs:
build_docs:
runs-on: docker
container:
image: docker.zeptotech.net/zeptotech/zig-yarn:1.1.0
credentials:
username: ${{ vars.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
steps:
- uses: actions/checkout@v4
- run: mkdir -p artifact/api
- run: (cd docs && corepack enable && yarn install)
- run: (cd docs && yarn docs:build)
- run: mv docs/.vitepress/dist/* artifact
- run: /zig/zig build docs
- run: mv zig-out/docs/* artifact/api
- uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
name: docs.zip
path: artifact

View file

@ -11,7 +11,7 @@
</h1> </h1>
<h4 align="center"> <h4 align="center">
<a href="https://zedd.zeptotech.net/zrm">Documentation</a> <a href="https://code.zeptotech.net/zedd/zrm">Documentation</a>
| |
<a href="https://zedd.zeptotech.net/zrm/api">API</a> <a href="https://zedd.zeptotech.net/zrm/api">API</a>
</h4> </h4>
@ -28,7 +28,7 @@ _ZRM_ provides a simple interface to relational databases in Zig. Define your re
## Versions ## Versions
ZRM 0.3.1 is made and tested with zig 0.13.0. ZRM 0.1.1 is made and tested with zig 0.13.0.
## Work in progress ## Work in progress
@ -41,7 +41,7 @@ ZRM aims to handle a lot for you, but it takes time to make. Have a look to [the
In your project directory: In your project directory:
```shell ```shell
$ zig fetch --save https://code.zeptotech.net/zedd/zrm/archive/v0.3.1.tar.gz $ zig fetch --save https://code.zeptotech.net/zedd/zrm/archive/v0.1.1.tar.gz
``` ```
In `build.zig`: In `build.zig`:
@ -55,14 +55,9 @@ const zrm = b.dependency("zrm", .{
exe.root_module.addImport("zrm", zrm.module("zrm")); exe.root_module.addImport("zrm", zrm.module("zrm"));
``` ```
### Documentation
A full documentation can be found on [zedd.zeptotech.net/zrm](https://zedd.zeptotech.net/zrm).
### Examples ### Examples
Some examples can be found in `tests` directory: Full examples can be found in `tests` directory:
- [`tests/example.zig`](https://code.zeptotech.net/zedd/zrm/src/branch/main/tests/example.zig)
- [`tests/repository.zig`](https://code.zeptotech.net/zedd/zrm/src/branch/main/tests/repository.zig) - [`tests/repository.zig`](https://code.zeptotech.net/zedd/zrm/src/branch/main/tests/repository.zig)
- [`tests/composite.zig`](https://code.zeptotech.net/zedd/zrm/src/branch/main/tests/composite.zig) - [`tests/composite.zig`](https://code.zeptotech.net/zedd/zrm/src/branch/main/tests/composite.zig)

View file

@ -1,19 +1,25 @@
.{ .{
.name = "zrm", .name = "zrm",
.version = "0.3.1", .version = "0.1.1",
.minimum_zig_version = "0.13.0", .minimum_zig_version = "0.13.0",
.dependencies = .{ .dependencies = .{
.zollections = .{ .zollections = .{
.url = "https://code.zeptotech.net/zedd/zollections/archive/v0.1.1.tar.gz", .url = "https://code.zeptotech.net/zedd/zollections/archive/v0.1.1.tar.gz",
.hash = "12200fe147879d72381633e6f44d76db2c8a603cda1969b4e474c15c31052dbb24b7", .hash = "12200fe147879d72381633e6f44d76db2c8a603cda1969b4e474c15c31052dbb24b7",
}, },
.pg = .{ .pg = .{
.url = "git+https://github.com/karlseguin/pg.zig?ref=zig-0.13#bc31e767e56218e8c51f9b0e40c581219b0ac62a", .url = "git+https://github.com/karlseguin/pg.zig?ref=zig-0.13#239a4468163a49d8c0d03285632eabe96003e9e2",
.hash = "1220795dd7be5bb28a29b7cf1a46233b0c370c158d7f4183c35db27f2b7db9564358", .hash = "1220a1d7e51e2fa45e547c76a9e099c09d06e14b0b9bfc6baa89367f56f1ded399a0",
}, },
}, },
.paths = .{ "build.zig", "build.zig.zon", "src", "README.md", "LICENSE" }, .paths = .{
"build.zig",
"build.zig.zon",
"src",
"README.md",
"LICENSE"
},
} }

View file

@ -1,8 +0,0 @@
root = true
[*]
end_of_line = lf
indent_style = tab
indent_size = 2
charset = utf-8
insert_final_newline = true

4
docs/.gitattributes vendored
View file

@ -1,4 +0,0 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated

8
docs/.gitignore vendored
View file

@ -1,8 +0,0 @@
# Yarn
.yarn/*
.pnp.*
node_modules/
# Vitepress
.vitepress/cache
.vitepress/dist

View file

@ -1,50 +0,0 @@
import {defineConfig} from 'vitepress';
// https://vitepress.dev/reference/site-config
export default defineConfig({
base: "/zrm",
title: "ZRM",
description: "Website of the ZRM library.",
themeConfig: {
logo: {
light: "/logo.svg",
dark: "/logo-dark.svg",
},
// https://vitepress.dev/reference/default-theme-config
nav: [
{text: "Home", link: "/"},
{text: "Documentation", link: "/docs"},
],
search: {
provider: "local",
},
sidebar: [
{
text: "Documentation",
link: "/docs",
items: [
{text: "Installation", link: "/docs/install"},
{text: "Database", link: "/docs/database"},
{text: "Repositories", link: "/docs/repositories"},
{text: "Queries", link: "/docs/queries"},
{text: "Insert & update", link: "/docs/insert-update"},
{text: "Relationships", link: "/docs/relationships"},
]
}
],
socialLinks: [
{icon: "git", link: "https://code.zeptotech.net/zedd/zrm"},
{icon: {
svg: '<?xml version="1.0" encoding="UTF-8"?><svg version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m375.47 87.498c-77.422 4e-3 -154.85-4e-3 -232.27 0.0248-7.7478 0.26053-14.842 6.6567-15.698 14.398-0.42683 3.6963-0.0855 7.4215-0.20025 11.131 0.0438 5.8956-0.15111 11.802 0.19405 17.689 1.0025 7.6834 8.0618 14.125 15.846 14.215 4.2666 0.16804 8.5375-4e-3 12.806 0.0652 47.273 4e-3 94.547 8e-3 141.82 0.0124-59.064 78.8-118.13 157.6-177.17 236.42-4.1696 5.8777-5.9165 13.246-5.5202 20.391 7e-4 4.3283-0.39389 8.8988 1.5211 12.928 2.6294 5.7046 8.6562 9.7403 14.978 9.7021 3.7094 0.0962 7.4213-0.0206 11.132 0.0372 79.299-8e-5 158.6-4.9e-4 237.9-0.0248 7.7662-0.24442 14.896-6.6555 15.736-14.42 0.4355-3.6719 0.0838-7.374 0.20189-11.059-0.0475-5.9531 0.16144-11.918-0.19405-17.862-1.0754-7.9984-8.7536-14.488-16.828-14.158-56.76-6.6e-4 -113.52-9.9e-4 -170.28-2e-3 59.168-78.755 118.35-157.5 177.49-236.27 4.1886-5.8581 6.0254-13.209 5.6328-20.359-0.033-4.3117 0.43852-8.8577-1.403-12.897-2.671-6.0034-9.0893-10.229-15.705-9.9512z" stroke-width="1.3882"/><path d="m412.39 47.672c-4.8102 0.08112-9.8691 1.9242-12.803 5.8954-3.1585 4.1727-3.3865 10.069-1.2002 14.73 1.5724 3.5822 3.9639 6.716 5.7997 10.158 9.2795 15.896 17.67 32.32 24.949 49.228 3.9724 9.2879 7.6063 18.957 10.803 28.551 3.92 11.803 7.3391 23.791 10.043 35.968 2.7053 12.009 4.5884 24.371 5.742 36.692 0.55268 5.8068 0.90486 11.631 1.0857 17.461 0.0329 1.6505 0.0973 3.5193 0.10888 5.2554 0.082 5.6578 3e-3 11.344-0.19389 16.944-0.10313 2.9514-0.28719 6.1037-0.51345 9.1456-0.59634 8.0708-1.5579 16.346-2.8077 24.349-1.5636 10.089-3.8089 20.181-6.4161 30.111-2.6012 9.8786-5.6052 19.65-8.9883 29.289-1.3489 3.8284-2.8061 7.7936-4.2981 11.637-4.4468 11.564-9.6421 22.97-15.217 34.094-5.476 11.002-11.561 21.865-17.891 32.467-1.4743 2.3235-2.817 4.7811-3.3944 7.4976-1.0035 4.1928-0.25168 8.936 2.6191 12.254 2.9126 3.4016 7.4828 5.0572 11.898 4.927 5.9702-0.0534 11.822-1.8401 17.148-4.4584 4.2796-2.1304 8.2468-4.9643 11.372-8.6053 2.8245-3.2611 5.4571-6.6826 8.1435-10.057 19.783-25.454 35.502-54.065 46.284-84.449 9.7389-27.315 15.5-56.057 16.96-85.021 1.0691-21.939-0.089-44.005-3.7557-65.669-6.031-35.934-18.69-70.751-37.215-102.13-9.2406-15.755-19.847-30.719-31.751-44.574-6.0592-6.1653-14.206-10.202-22.74-11.48-1.2512-0.14827-2.5111-0.23355-3.7715-0.20898z" stroke-width="1.3461"/><path d="m99.614 464.33c4.8102-0.0811 9.8691-1.9242 12.803-5.8954 3.1585-4.1727 3.3865-10.069 1.2002-14.73-1.5724-3.5822-3.9639-6.716-5.7997-10.158-9.2795-15.896-17.67-32.32-24.949-49.228-3.9724-9.2879-7.6063-18.957-10.803-28.551-3.92-11.803-7.3391-23.791-10.043-35.968-2.7053-12.009-4.5884-24.371-5.742-36.692-0.55268-5.8068-0.90486-11.631-1.0857-17.461-0.0329-1.6505-0.0973-3.5193-0.10888-5.2554-0.082-5.6578-3e-3 -11.344 0.19389-16.944 0.10313-2.9514 0.28719-6.1037 0.51345-9.1456 0.59634-8.0708 1.5579-16.346 2.8077-24.349 1.5636-10.089 3.8089-20.181 6.4161-30.111 2.6012-9.8786 5.6052-19.65 8.9883-29.289 1.3489-3.8284 2.8061-7.7936 4.2981-11.637 4.4468-11.564 9.6421-22.97 15.217-34.094 5.476-11.002 11.561-21.865 17.891-32.467 1.4743-2.3235 2.817-4.7811 3.3944-7.4976 1.0035-4.1928 0.25168-8.936-2.6191-12.254-2.9126-3.4016-7.4828-5.0572-11.898-4.927-5.9702 0.0534-11.822 1.8401-17.148 4.4584-4.2796 2.1304-8.2468 4.9643-11.372 8.6053-2.8245 3.2611-5.4571 6.6826-8.1435 10.057-19.783 25.454-35.502 54.065-46.284 84.449-9.7389 27.315-15.5 56.057-16.96 85.021-1.0691 21.939 0.089 44.005 3.7557 65.669 6.031 35.934 18.69 70.751 37.215 102.13 9.2406 15.755 19.847 30.719 31.751 44.574 6.0592 6.1653 14.206 10.202 22.74 11.48 1.2512 0.14827 2.5111 0.23355 3.7715 0.20898z" stroke-width="1.3461"/></svg>',
}, link: "https://code.zeptotech.net/zedd"},
],
footer: {
message: "<a class='zeptotech' href='https://zeptotech.fr' target='_blank' ><img src='/zrm/zeptotech.svg' alt='Zeptotech' /></a> Powered by Zeptotech"
}
},
});

View file

@ -1,17 +0,0 @@
// https://vitepress.dev/guide/custom-theme
import {h} from 'vue';
import type {Theme} from 'vitepress';
import DefaultTheme from 'vitepress/theme';
import './style.css';
export default {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
// https://vitepress.dev/guide/extending-default-theme#layout-slots
})
},
enhanceApp({app, router, siteData})
{
}
} satisfies Theme;

View file

@ -1,170 +0,0 @@
/**
* Customize default theme styling by overriding CSS variables:
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
*/
/**
* Colors
*
* Each colors have exact same color scale system with 3 levels of solid
* colors with different brightness, and 1 soft color.
*
* - `XXX-1`: The most solid color used mainly for colored text. It must
* satisfy the contrast ratio against when used on top of `XXX-soft`.
*
* - `XXX-2`: The color used mainly for hover state of the button.
*
* - `XXX-3`: The color for solid background, such as bg color of the button.
* It must satisfy the contrast ratio with pure white (#ffffff) text on
* top of it.
*
* - `XXX-soft`: The color used for subtle background such as custom container
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
* on top of it.
*
* The soft color must be semi transparent alpha channel. This is crucial
* because it allows adding multiple "soft" colors on top of each other
* to create a accent, such as when having inline code block inside
* custom containers.
*
* - `default`: The color used purely for subtle indication without any
* special meanings attached to it such as bg color for menu hover state.
*
* - `brand`: Used for primary brand colors, such as link text, button with
* brand theme, etc.
*
* - `tip`: Used to indicate useful information. The default theme uses the
* brand color for this by default.
*
* - `warning`: Used to indicate warning to the users. Used in custom
* container, badges, etc.
*
* - `danger`: Used to show error, or dangerous message to the users. Used
* in custom container, badges, etc.
* -------------------------------------------------------------------------- */
:root
{
--vp-c-default-1: var(--vp-c-gray-1);
--vp-c-default-2: var(--vp-c-gray-2);
--vp-c-default-3: var(--vp-c-gray-3);
--vp-c-default-soft: var(--vp-c-gray-soft);
--vp-c-brand-1: var(--vp-c-indigo-1);
--vp-c-brand-2: var(--vp-c-indigo-2);
--vp-c-brand-3: var(--vp-c-indigo-3);
--vp-c-brand-soft: var(--vp-c-indigo-soft);
--vp-c-tip-1: var(--vp-c-brand-1);
--vp-c-tip-2: var(--vp-c-brand-2);
--vp-c-tip-3: var(--vp-c-brand-3);
--vp-c-tip-soft: var(--vp-c-brand-soft);
--vp-c-warning-1: var(--vp-c-yellow-1);
--vp-c-warning-2: var(--vp-c-yellow-2);
--vp-c-warning-3: var(--vp-c-yellow-3);
--vp-c-warning-soft: var(--vp-c-yellow-soft);
--vp-c-danger-1: var(--vp-c-red-1);
--vp-c-danger-2: var(--vp-c-red-2);
--vp-c-danger-3: var(--vp-c-red-3);
--vp-c-danger-soft: var(--vp-c-red-soft);
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root
{
--vp-button-brand-border: transparent;
--vp-button-brand-text: var(--vp-c-white);
--vp-button-brand-bg: var(--vp-c-brand-3);
--vp-button-brand-hover-border: transparent;
--vp-button-brand-hover-text: var(--vp-c-white);
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
--vp-button-brand-active-border: transparent;
--vp-button-brand-active-text: var(--vp-c-white);
--vp-button-brand-active-bg: var(--vp-c-brand-1);
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root
{
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#22aeff 30%,
#00f1cb
);
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
#22aeff 50%,
#00f1cb 50%
);
--vp-home-hero-image-filter: blur(44px);
}
@media (min-width: 640px)
{
:root
{
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px)
{
:root
{
--vp-home-hero-image-filter: blur(68px);
}
}
.VPFeatures .icon svg
{
font-size: 32px;
}
/**
* Component: Custom Block
* -------------------------------------------------------------------------- */
:root
{
--vp-custom-block-tip-border: transparent;
--vp-custom-block-tip-text: var(--vp-c-text-1);
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch
{
--docsearch-primary-color: var(--vp-c-brand-1) !important;
}
/**
* Component: Footer
* -------------------------------------------------------------------------- */
footer a.zeptotech
{
display: block;
margin: auto;
}
footer img
{
display: block;
margin: auto;
width: 3em;
max-width: 95%;
}

View file

@ -1 +0,0 @@
nodeLinker: node-modules

View file

@ -1,3 +0,0 @@
# ZRM :: Documentation
This is the documentation website of the ZRM library.

View file

@ -1,82 +0,0 @@
# Database
As a database-to-zig mapper, ZRM obviously needs to connect to a database to operate. All interactions with ZRM features will first need to define a database connection.
::: info
ZRM currently only supports PostgreSQL through [pg.zig](https://github.com/karlseguin/pg.zig). More DBMS's support is [planned](https://code.zeptotech.net/zedd/zrm/issues/8).
:::
## Connection
As ZRM is currently using [pg.zig](https://github.com/karlseguin/pg.zig) to connect to PostgreSQL databases, you can find a full documentation and example on the [pg.zig documentation](https://github.com/karlseguin/pg.zig#example).
```zig
const database = try pg.Pool.init(allocator, .{
.connect = .{
.host = "localhost",
.port = 5432,
},
.auth = .{
.username = "zrm",
.password = "zrm",
.database = "zrm",
},
.size = 5,
});
```
## Connector
ZRM does not use opened connections directly. All features use a generic interface called a `Connector`. A connector manages how connections are opened and released for a group of operations. There are currently two types of connectors in ZRM.
### Pool connector
The pool connector simply use a `pg.Pool` to get connections when needed. The only requirement is an opened database pool from pg.zig.
```zig
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
```
### Session connector
A session connector use a single connection while it is initialized, which is very useful when you want to perform a group of operations in a transaction. The deinitialization releases the connection.
```zig
// Start a new session.
var session = try zrm.Session.init(database);
defer session.deinit();
```
Using sessions, you can start transactions and use savepoints.
::: warning
You probably want to rollback all active transactions in `defer`, so that none remain active after leaving the active branch.
:::
```zig
// Start a new session.
var session = try zrm.Session.init(database);
defer {
// Rollback all active transactions that remain active.
session.rollbackAll();
session.deinit();
};
try session.beginTransaction();
// Do something.
try session.savepoint("my_savepoint");
// Do something else.
try session.rollbackTo("my_savepoint");
// Do a third thing.
try session.commitTransaction();
// or
try session.rollbackTransaction();
```

View file

@ -1,110 +0,0 @@
# Documentation
Welcome to the ZRM documentation!
ZRM is a try to make a fast and efficient zig-native [ORM](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) (Object Relational Mapper). With ZRM, you can define your zig models structures and easily link them to your database tables.
ZRM is using [compile-time features](https://ziglang.org/documentation/0.13.0/#toc-comptime) of the zig language to do a lot of generic work you clearly don't want to do.
## How does it look like?
```zig
/// User model.
pub const User = struct {
pub const Table = struct {
id: i32,
name: []const u8,
pub const Insert = struct {
name: []const u8,
};
};
id: i32,
name: []const u8,
info: ?UserInfo = null,
};
/// Repository of User model.
pub const UserRepository = zrm.Repository(User, User.Table, .{
.table = "example_users",
.insertShape = User.Table.Insert,
.key = &[_][]const u8{"id"},
.fromSql = zrm.helpers.TableModel(User, User.Table).copyTableToModel,
.toSql = zrm.helpers.TableModel(User, User.Table).copyModelToTable,
});
/// Relationships of User model.
pub const UserRelationships = UserRepository.relationships.define(.{
.info = UserRepository.relationships.one(UserInfoRepository, .{
.reverse = .{},
}),
});
/// User info model.
pub const UserInfo = struct {
pub const Table = struct {
user_id: i32,
birthdate: i64,
pub const Insert = struct {
user_id: i32,
birthdate: i64,
};
};
user_id: i32,
birthdate: i64,
user: ?*User = null,
};
/// Repository of UserInfo model.
pub const UserInfoRepository = zrm.Repository(UserInfo, UserInfo.Table, .{
.table = "example_users_info",
.insertShape = UserInfo.Table.Insert,
.key = &[_][]const u8{"user_id"},
.fromSql = zrm.helpers.TableModel(UserInfo, UserInfo.Table).copyTableToModel,
.toSql = zrm.helpers.TableModel(UserInfo, UserInfo.Table).copyModelToTable,
});
/// Relationships of UserInfo model.
pub const UserInfoRelationships = UserInfoRepository.relationships.define(.{
.user = UserInfoRepository.relationships.one(UserRepository, .{
.direct = .{
.foreignKey = "user_id",
},
}),
});
// Initialize a query to get users.
var firstQuery = UserRepository.QueryWith(
// Retrieve info of users.
&[_]zrm.relationships.Relationship{UserRelationships.info}
).init(std.testing.allocator, poolConnector.connector(), .{});
// We want to get the user with ID 2.
try firstQuery.whereKey(2);
defer firstQuery.deinit();
// Executing the query and getting its result.
var firstResult = try firstQuery.get(std.testing.allocator);
defer firstResult.deinit();
if (firstResult.first()) |myUser| {
// A user has been found.
std.debug.print("birthdate timestamp: {d}\n", .{user.info.birthdate});
// Changing user name.
myUser.name = "zrm lover";
// Saving the altered user.
const saveResult = UserRepository.save(allocator, poolConnector.connector(), myUser);
defer saveResult.deinit();
} else {
std.debug.print("no user with id 2 :-(\n", .{});
}
```
## Discover
This documentation will help you to discover all the features provided by ZRM and how you can use them in your project. We'll cover [installation](/docs/install), [database connection](/docs/database), [repository declaration](/docs/repositories), [queries](/docs/queries), [insertions and updates](/docs/insert-update), and [relationships](/docs/relationships). Most examples are based on a test file, [`tests/example.zig`](https://code.zeptotech.net/zedd/zrm/src/branch/main/tests/example.zig), which demonstrates and tests models, repositories, relationships and queries.

View file

@ -1,147 +0,0 @@
# Insert & update
To define and use inserts and updates, you must have a fully defined [repository](/docs/repositories). In this tutorial, we'll be assuming that we have a defined repository for a user model, as it's defined in [this section](/docs/repositories.html#define-a-repository).
Executing inserts and updates also require to [set up a connection to your database](/docs/database). We'll also be assuming that we have a working database connector set up, as it's defined in [this section](/docs/database#pool-connector).
## Insert
Just like queries, we can insert models using the type `Insert` published on repositories. With this type, the inserted shape is the default one of the repository. You can customize this structure by using `InsertCustom` function.
```zig
const insertQuery = UserRepository.Insert.init(allocator, poolConnector.connector());
// or
const insertQuery = UserRepository.InsertCustom(struct {
id: i32,
name: []const u8,
}).init(allocator, poolConnector.connector());
```
If you just need to insert a model without any other parameters, repositories provide a `create` function which do just that. The given model **will be altered** with inserted row data.
```zig
const results = UserRepository.create(allocator, poolConnector.connector(), model);
defer results.deinit();
```
### Values
With an insert query, we can pass our values to insert with the `values` function. This looks like [`set` function of update queries](#values-1).
```zig
// Insert a single model.
try insertQuery.values(model);
// Insert an array of models.
try insertQuery.values(&[_]Model{firstModel, secondModel});
// Insert a table-shaped structure.
try insertQuery.values(table);
// Insert an array of table-shaped structures.
try insertQuery.values(&[_]Model.Table{firstTable, secondTable});
// Insert a structure matching InsertShape.
try insertQuery.values(insertShapeStructure);
// Insert an array of structures matching InsertShape.
try insertQuery.values(&[_]Model.Table.Insert{firstInsertShape, secondInsertShape});
```
### Returning
It's often useful to retrieve inserted data after the query. One use case would for example to get the inserted auto-increment IDs of the models. We can do this using the `returningX` functions of the insert query builder.
::: danger
Never put user-sent values as selected columns. This could lead to severe security issues (like [SQL injections](https://en.wikipedia.org/wiki/SQL_injection)).
:::
#### Returning all
This will return all the columns of the inserted rows.
```zig
try insertQuery.values(...);
insertQuery.returningAll();
```
#### Returning columns
This will return all the provided columns of the inserted rows.
```zig
try insertQuery.values(...);
insertQuery.returningColumns(&[_][]const u8{"id", "name"});
```
#### Raw returning
We can also directly provide raw `RETURNING` clause content.
```zig
try insertQuery.values(...);
insertQuery.returning("id, label AS name");
```
### Results
We can perform the insertion by running `insert` on the insert query.
```zig
const results = try insertQuery.insert(allocator);
defer results.deinit();
```
The results of an insert query are the same as normal queries. You can find the documentation about it in [its dedicated section](/docs/queries#results).
## Update
To make an update query, we must provide the structure of the updated columns (called update shape).
```zig
const updateQuery = UserRepository.Update(struct { name: []const u8 }).init(allocator, poolConnector.connector());
```
If you just need to update a model without any other parameters, repositories provide a `save` function which do just that. The given model **will be altered** with updated row data.
```zig
const results = UserRepository.save(allocator, poolConnector.connector(), model);
defer results.deinit();
```
### Values
With an update query, we can set our updated values with the `set` function. This looks like [`values` function of insert queries](#values).
```zig
// Set data of a single model.
try updateQuery.set(model);
// Set data of an array of models.
try updateQuery.values(&[_]Model{firstModel, secondModel});
// Set data of a table-shaped structure.
try updateQuery.values(table);
// Set data of an array of table-shaped structures.
try updateQuery.values(&[_]Model.Table{firstTable, secondTable});
// Set data of a structure matching UpdateShape.
try updateQuery.values(myUpdate);
// Set data of an array of structures matching UpdateShape.
try updateQuery.values(&[_]UpdateStruct{myFirstUpdate, mySecondUpdate});
```
### Conditions
The conditions building API is the same as normal queries. You can find the documentation about it in [its dedicated section](/docs/queries#conditions).
### Returning
The returning columns API is the same as insert queries. You can find the documentation about it in [its dedicated section](#returning).
### Results
We can perform the update by running `update` on the update query.
```zig
const results = try updateQuery.update(allocator);
defer results.deinit();
```
The results of an update query are the same as normal queries. You can find the documentation about it in [its dedicated section](/docs/queries#results).

View file

@ -1,41 +0,0 @@
# Installation
You can easily install ZRM using the `zig fetch` command:
```shell
$ zig fetch --save https://code.zeptotech.net/zedd/zrm/archive/v0.3.1.tar.gz
```
::: info
You can tweak the version if you want to use something else than the latest stable one, you can find all available versions in [the tags page](https://code.zeptotech.net/zedd/zrm/tags) on the repository.
:::
This should add something like the following in `build.zig.zon` dependencies:
```zon
.{
// ...
.dependencies = .{
// ...
.zrm = .{
.url = "https://code.zeptotech.net/zedd/zrm/archive/v0.3.1.tar.gz",
.hash = "12200fe...",
},
// ...
},
// ...
}
```
Then, you can add the `zrm` module to your project build in `build.zig`.
```zig
// Add zrm dependency.
const zrm = b.dependency("zrm", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zrm", zrm.module("zrm"));
```
You can now start to use ZRM! Why not trying to define [your first repository](/docs/repositories)?

View file

@ -1,209 +0,0 @@
# Queries
To define and use queries, you must have a fully defined [repository](/docs/repositories). In this tutorial, we'll be assuming that we have a defined repository for a user model, as it's defined in [this section](/docs/repositories.html#define-a-repository).
Executing queries also require to [set up a connection to your database](/docs/database). We'll also be assuming that we have a working database connector set up, as it's defined in [this section](/docs/database#pool-connector).
## Query building
ZRM repositories provide a model query builder by default. We can access it from the published type `Query`.
### Basics
Let's start with the most simple query we can make.
```zig
var myFirstQuery = UserRepository.Query.init(
allocator, poolConnector.connector(), .{}
);
defer myFirstQuery.deinit();
```
As you can see, we're creating a new query instance with our [previously defined connector](/docs/database#pool-connector). The last argument has no mandatory fields, but here are all the available options:
- `select`: a raw query part where we can set all columns that we want to select. By default, all columns of the table are retrieved (in the form of `"table_name".*`).
- `join`: a raw query part where we can set all joined tables. By default, nothing is set (~ empty string) and there will be no `JOIN` clause.
- `where`: a raw query part where we can set all the conditions to apply to the query. By default, nothing is set and there will be no `WHERE` clause.
::: warning
It is currently **NOT recommended** to use these variables to build your queries. This configuration object is experimental and might be used later to define comptime-known parts of the query, and may also be entirely removed.
:::
Based on the current configuration, we can execute this query as is, and ZRM will try to get **all** models in the defined table.
### Conditions
::: warning
Calling any of the `whereX` functions on a query overrides anything that has been previously set. If you call `whereValue` two times, only the secondly defined condition will be kept. If you need to have multiple conditions at once, you should use the [conditions builder](/docs/queries#conditions-builder).
:::
#### Simple value
Add a condition between a column and a runtime value. The type of the value must be provided as a first argument. Any valid SQL operator is accepted.
```zig
try query.whereValue(usize, "id", "!=", 1);
try query.whereValue(f32, "\"products\".\"amount\"", "<", 35.25);
```
#### Primary keys
Add a condition on the primary keys. If the primary key is composite, a structure with all the keys values is expected.
```zig
// Find the model with ID 1.
try query.whereKey(1);
// Find the model with primary key ('foo', 'bar').
try compositeQuery.whereKey(.{ .identifier = "foo", .name = "bar" });
```
The provided argument can also be an array of keys.
```zig
// Find models with ID 1 or 3.
try query.whereKey(&[_]usize{1, 3});
// Find models with primary key ('foo', 'bar') or ('baz', 'test').
try compositeQuery.whereKey(&[_]struct{identifier: []const u8, name: []const u8}{
.{ .identifier = "foo", .name = "bar" },
.{ .identifier = "baz", .name = "test" }
});
```
If you just need to get models from their ID without any other condition, repositories provide a `find` function which do just that.
```zig
const models = UserRepository.find(allocator, poolConnector.connector(), 1);
```
#### Array
Add a `WHERE column IN` condition.
```zig
try query.whereIn(usize, "id", &[_]usize{1, 2});
```
#### Column
Add a condition between two columns in the query. The columns name all must be comptime-known.
```zig
try query.whereColumn("products.amount", "<", "clients.available_amount");
```
#### Conditions builder
Sometimes, we need to build complex conditions. For this purpose, we can use the conditions builder.
The recommended way to initialize a conditions builder is to use the query, as the built conditions will be freed when the query is deinitialized.
```zig
try query.newCondition()
```
We can also directly use the conditions builder with our own allocator.
```zig
try zrm.conditions.Builder.init(allocator);
```
With the conditions builder, we can build complex conditions with AND / OR and different types of tests.
```zig
query.where(
try query.newCondition().@"or"(&[_]zrm.RawQuery{
try query.newCondition().value(usize, "id", "=", 1),
try query.newCondition().@"and"(&[_]zrm.RawQuery{
try query.newCondition().in(usize, "id", &[_]usize{100000, 200000, 300000}),
try query.newCondition().@"or"(&[_]zrm.RawQuery{
try query.newCondition().value(f64, "amount", ">", 12.13),
try query.newCondition().value([]const u8, "name", "=", "test"),
})
}),
})
);
// will produce the following WHERE clause:
// WHERE (id = ? OR (id IN (?,?,?) AND (amount > ? OR name = ?)))
```
#### Raw where
To set a raw `WHERE` clause content, we can use the `where` function.
```zig
query.where(zrm.RawQuery{
.sql = "id = ?",
.params = &[_]zrm.RawQueryParameter{.{.integer = 1}}
});
```
### Joins
::: warning
ZRM currently only supports **raw joins** definitions. Real join definition functions are expected to come in next releases.
:::
To set a raw `JOIN` clause, we can use the `join` function.
```zig
query.join(zrm.RawQuery{
.sql = "INNER JOIN foo ON user.id = foo.user_id",
.params = &[0]zrm.RawQueryParameter{}
});
// or
query.join(zrm.RawQuery{
.sql = "LEFT JOIN foo ON foo.id = ?",
.params = &[_]zrm.RawQueryParameter{.{.integer = 1}}
});
```
### Selects
::: danger
**Never** put user-sent values as selected columns. This could lead to severe security issues (like [SQL injections](https://en.wikipedia.org/wiki/SQL_injection)).
:::
#### Columns
We can select specific columns in a query with `selectColumns`. At least one selected column is required.
```zig
try query.selectColumns(&[_][]const u8{"id", "label AS name", "amount"});
```
#### Raw select
To set a raw `SELECT` clause content, we can use the `select` function.
```zig
query.where(zrm.RawQuery{
.sql = "id, label AS name, amount",
.params = &[0]zrm.RawQueryParameter{}
});
```
## Results
When our query is fully configured, we can finally call `get` to retrieve the results. We must provide an allocator to hold all the allocated models and their values. The results don't require the query to be kept, so we **can** run `query.deinit()` after getting the results without losing what has been retrieved.
```zig
var result = try query.get(allocator);
defer result.deinit();
```
The result structure allows to access the models list or the first model directly, if we just want a single one (or made sure that only one has been retrieved).
```zig{4,8}
var result = try query.get(allocator);
defer result.deinit();
if (result.first()) |model| {
// Do something with the first model.
}
for (result.models) |model| {
// Do something with all models.
}
```
The query builder allows you to get zig models from the database, but you may also need to [store them in database](/docs/insert-update) after creating or altering them.

View file

@ -1,184 +0,0 @@
# Relationships
To define and use relationships, you must have a fully defined [repository](/docs/repositories). In this tutorial, we'll be assuming that we have a defined repository for a user model, as it's defined in [this section](/docs/repositories.html#define-a-repository).
Executing queries also require to [set up a connection to your database](/docs/database). We'll also be assuming that we have a working database connector set up, as it's defined in [this section](/docs/database#pool-connector).
## What is a relationship?
Before starting to define our relationships, let's try to define what they are. A relationship is a logical connection between models. In real-world applications, models are often connected between each other, and that's why we even made _relational_ databases.
If we are trying to create an easy model of a chat room, we could have two main entities:
- the chatters, that we will then call "users".
- the messages.
There is a relationship between a message and a user, because a message is always written by _someone_. In relational databases, we use foreign keys to represent this relationship (we would store a `user_id` in the `messages` table). But in programming languages, we manipulate structures and objects directly, so it can be a pain to perform operations with indexed maps or arrays.
That's why ORM are now so common in object-oriented languages. They greatly simplify the use of database-stored models, even sometimes completely hiding this fact. Zig structures are sometimes quite similar to objects of object-oriented languages, so simplifying interactions between zig structures and database tables is important.
## Define relationships
In ZRM, we define relationships on a repository. The defined relationships are stored in a comptime-known structure, reusable when building a model query.
```zig
const UserRelationships = UserRepository.relationships.define(.{
// Here, we can define the relationships of the User model.
});
```
The field where the related models will be stored after retrieval is the one with the same name in the relationships structure.
```zig
const UserRelationships = UserRepository.relationships.define(.{
// Will put the related model in `relatedModel` field of User structure:
.relatedModel = UserRepository.relationships.one(.{...}),
// Will put the related models in `relatedModels` field of User structure:
.relatedModels = UserRepository.relationships.many(.{...}),
});
```
## `one` relationships
This type of relationship is used when only a single model is related. In our chat example, the relationship type between a message and a user is "one", as there's only one message author.
### Direct
![Direct one relation diagram](/relationships/one-direct.svg)
The direct one relationship uses a local foreign key to get the related model. In other libraries, this type of relationship can be referred as "belongs to". It has two parameters:
- **mandatory** `foreignKey`: name of the field / column where the related model key is stored.
- _optional_ `modelKey`: name of the key of the related model. When none is provided, the default related model key name is used (it's usually the right choice).
```zig
const MessageRelationships = MessageRepository.relationships.define(.{
.user = MessageRepository.relationships.one(UserRepository, .{
.direct => .{
.foreignKey = "user_id",
},
}),
});
```
### Reverse
![Reverse one relation diagram](/relationships/one-reverse.svg)
The reverse one relationship uses a distant foreign key to get the related model. It can be used to get related models when they hold a foreign key to the origin model. In other libraries, this type of relationship can be referred as "has one". It has two parameters:
- _optional_ `foreignKey`: name of the field / column where the related model key is stored. When none is provided, the default related model key name is used.
- _optional_ `modelKey`: name of the key of the origin model. When none is provided, the default origin model key name is used (it's usually the right choice).
```zig
const UserRelationships = UserRepository.relationships.define(.{
.info = UserRepository.relationships.one(UserInfoRepository, .{
.reverse = .{
.foreignKey = "user_id", // this is optional if "user_id" is the defined primary key of UserInfoRepository.
},
}),
});
```
### Through
![Through one relation diagram](/relationships/one-through.svg)
The through one relationship uses a pivot table to get the related model. It can be used to get related models when the foreign key is hold by an intermediate table. In other libraries, this type of relationship can be referred as "has one through". It has five parameters:
- **mandatory** `table`: name of the pivot / intermediate / join table.
- _optional_ `foreignKey`: name of the foreign key in the origin table. When none is provided, the default origin model key name is used (it's usually the right choice).
- **mandatory** `joinForeignKey`: name of the foreign key in the intermediate table. Its value will match the one in `foreignKey`.
- **mandatory** `joinModelKey`: name of the related model key name in the intermediate table. Its value will match the one in `modelKey`.
- _optional_ `modelKey`: name of the model key in the related table. When none is provided, the default related model key name is used (it's usually the right choice).
```zig
const MessageRelationships = MessageRepository.relationships.define(.{
.user = MessageRepository.relationships.one(UserRepository, .{
.direct = .{
.foreignKey = "user_id",
}
}),
.user_picture = MessageRepository.relationships.one(MediaRepository, .{
.through = .{
.table = "example_users",
.foreignKey = "user_id",
.joinForeignKey = "id",
.joinModelKey = "picture_id",
},
}),
});
```
## `many` relationships
This type of relationship is used when only a many models are related. In our chat example, the relationship type between a user and messages is "many", as an author can write multiple messages.
### Direct
![Direct many relation diagram](/relationships/many-direct.svg)
The direct many relationship uses a distant foreign key to get related models. It's often used at the opposite side of a direct one relationship. In other libraries, this type of relationship can be referred as "has many". It has two parameters:
- **mandatory** `foreignKey`: name of the field / column where the origin model key is stored.
- _optional_ `modelKey`: name of the key of the origin model. When none is provided, the default origin model key name is used (it's usually the right choice).
```zig
const UserRelationships = UserRepository.relationships.define(.{
.messages = UserRepository.relationships.many(MessageRepository, .{
.direct = .{
.foreignKey = "user_id",
},
}),
});
```
### Through
![Through many relation diagram](/relationships/many-through.svg)
The through many relationship uses a pivot table to get the related models. It can be used to get related models when the foreign key is hold by an intermediate table. In other libraries, this type of relationship can be referred as "belongs to many". It has five parameters:
- **mandatory** `table`: name of the pivot / intermediate / join table.
- _optional_ `foreignKey`: name of the foreign key in the origin table. When none is provided, the default origin model key name is used (it's usually the right choice).
- **mandatory** `joinForeignKey`: name of the foreign key in the intermediate table. Its value will match the one in `foreignKey`.
- **mandatory** `joinModelKey`: name of the related model key name in the intermediate table. Its value will match the one in `modelKey`.
- _optional_ `modelKey`: name of the model key in the related table. When none is provided, the default related model key name is used (it's usually the right choice).
```zig
const MessageRelationships = MessageRepository.relationships.define(.{
.medias = MessageRepository.relationships.many(MediaRepository, .{
.through = .{
.table = "example_messages_medias",
.joinModelKey = "message_id",
.joinForeignKey = "media_id",
},
}),
});
```
## Query related models
Now that our relationships are defined, we can query our models with their relationships directly with the `QueryWith` function of the repository. `QueryWith` takes an array of `Relationship` structures, which are created by `Repository.relationships.define`. To get relationships along with the models, you just need to fill this array with the requested relationships.
```zig
// Initialize a user query, with their messages.
var userQuery = UserRepository.QueryWith(
// Get messages of retrieved users.
&[_]zrm.relationships.Relationship{UserRelationships.messages}
).init(std.testing.allocator, poolConnector.connector(), .{});
try userQuery.whereKey(1);
defer userQuery.deinit();
// Get the queried user with their messages.
var userResult = try userQuery.get(std.testing.allocator);
defer userResult.deinit();
if (userResult.first()) |user| {
// The user has been found, showing their messages.
for (user.messages.?) |message| {
std.debug.print("{s}: {s}", .{user.name, message.text});
}
}
```

View file

@ -1,114 +0,0 @@
# Repositories
The first concept that ZRM introduces is a pretty common one: **repositories**. Repositories are the main interface for you to access what is stored in database, or to store anything in it. It is the _bridge_ between your model (a normal zig structure) and the table in database.
## Define a model
There's nothing special to do to define a model to use with ZRM. Let's start with a simple user model.
```zig
const User = struct {
id: i32,
name: []const u8,
};
```
It's a quite simple structure, but you'll quickly add more things when working with it, so let's define another structure that will hold the structure of the user in database.
```zig
const User = struct {
pub const Table = struct {
id: i32,
name: []const u8,
};
id: i32,
name: []const u8,
};
```
For now, `User` and `User.Table` are the same, but this will change as we add more features to our user.
## Define a repository
Now, let's define a repository for our `User` model.
```zig
const UserRepository = zrm.Repository(User, User.Table, .{
// ...
});
```
A repository is mainly based on 2 structures: the model and the table. These are the first two arguments. Next, it's a configuration object, with the following mandatory values:
- `table`: the table in which the models are stored.
- `insertShape`: the inserted columns by default. See [Insert & update](/docs/insert-update#insert) for more info.
- `key`: array of fields / columns to use as primary keys.
- `fromSql` / `toSql`: functions to convert tables to models and models to tables, which are used when getting and storing data.
Let's define all these fields:
```zig
const User = struct {
pub const Table = struct {
id: i32,
name: []const u8,
pub const Insert = struct {
name: []const u8,
};
};
id: i32,
name: []const u8,
};
const UserRepository = zrm.Repository(User, User.Table, .{
.table = "example_users",
.insertShape = User.Table.Insert,
.key = &[_][]const u8{"id"},
.fromSql = userFromSql,
.toSql = userToSql,
});
fn userFromSql(table: User.Table) User {
return .{
.id = table.id,
.name = table.name,
};
}
fn userToSql(user: User) User.Table {
return .{
.id = user.id,
.name = user.name,
};
}
```
We created a new structure for `insertShape`: it's the same as `User.Table`, but without the ID, as it will be automatically filled by the database when inserting (assuming that its column is defined as _auto-incrementing on insert_).
You may see that current implementation of `userFromSql` and `userToSql` is a bit useless. Luckily, ZRM provides a helper function to automatically generate them.
```zig{7,8}
const UserRepository = zrm.Repository(User, User.Table, .{
.table = "example_users",
.insertShape = User.Table.Insert,
.key = &[_][]const u8{"id"},
.fromSql = zrm.helpers.TableModel(User, User.Table).copyTableToModel,
.toSql = zrm.helpers.TableModel(User, User.Table).copyModelToTable,
});
```
It's finally done! Our repository is fully defined. As you can see we defined the following:
- where to store the models in database (which table).
- what will be inserted.
- what are the primary keys of the model.
- how to format stored data and how to get them from their stored form.
These are all the info required by ZRM to know how to deal with your models. We can now have a look to [how to retrieve models from database](/docs/queries).

View file

@ -1,32 +0,0 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "ZRM"
text: "Zig Relational Mapper"
tagline: Easy interactions with your database
image:
light: "/logo.svg"
dark: "/logo-dark.svg"
alt: "ZRM logo"
actions:
- theme: brand
text: Documentation
link: /docs
- theme: alt
text: API
link: https://zedd.zeptotech.net/zrm/api
features:
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M216 80c0 26.51-39.4 48-88 48s-88-21.49-88-48s39.4-48 88-48s88 21.49 88 48" opacity="0.2"/><path d="M128 24c-53.83 0-96 24.6-96 56v96c0 31.4 42.17 56 96 56s96-24.6 96-56V80c0-31.4-42.17-56-96-56m80 104c0 9.62-7.88 19.43-21.61 26.92C170.93 163.35 150.19 168 128 168s-42.93-4.65-58.39-13.08C55.88 147.43 48 137.62 48 128v-16.64c17.06 15 46.23 24.64 80 24.64s62.94-9.68 80-24.64ZM69.61 53.08C85.07 44.65 105.81 40 128 40s42.93 4.65 58.39 13.08C200.12 60.57 208 70.38 208 80s-7.88 19.43-21.61 26.92C170.93 115.35 150.19 120 128 120s-42.93-4.65-58.39-13.08C55.88 99.43 48 89.62 48 80s7.88-19.43 21.61-26.92m116.78 149.84C170.93 211.35 150.19 216 128 216s-42.93-4.65-58.39-13.08C55.88 195.43 48 185.62 48 176v-16.64c17.06 15 46.23 24.64 80 24.64s62.94-9.68 80-24.64V176c0 9.62-7.88 19.43-21.61 26.92"/></g></svg>'
title: Database models mapper
details: Connect zig models to your database.
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M64 112v32a8 8 0 0 1-8 8H24a8 8 0 0 1-8-8v-32a8 8 0 0 1 8-8h32a8 8 0 0 1 8 8m144-72h-48a8 8 0 0 0-8 8v48a8 8 0 0 0 8 8h48a8 8 0 0 0 8-8V48a8 8 0 0 0-8-8m0 112h-48a8 8 0 0 0-8 8v48a8 8 0 0 0 8 8h48a8 8 0 0 0 8-8v-48a8 8 0 0 0-8-8" opacity="0.2"/><path d="M160 112h48a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16h-48a16 16 0 0 0-16 16v16h-16a24 24 0 0 0-24 24v32H72v-8a16 16 0 0 0-16-16H24a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h32a16 16 0 0 0 16-16v-8h32v32a24 24 0 0 0 24 24h16v16a16 16 0 0 0 16 16h48a16 16 0 0 0 16-16v-48a16 16 0 0 0-16-16h-48a16 16 0 0 0-16 16v16h-16a8 8 0 0 1-8-8V88a8 8 0 0 1 8-8h16v16a16 16 0 0 0 16 16M56 144H24v-32h32zm104 16h48v48h-48Zm0-112h48v48h-48Z"/></g></svg>'
title: Models relationships definition
details: Get related models in a single well-defined structure.
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m96 240l16-80l-64-24L160 16l-16 80l64 24Z" opacity="0.2"/><path d="M215.79 118.17a8 8 0 0 0-5-5.66L153.18 90.9l14.66-73.33a8 8 0 0 0-13.69-7l-112 120a8 8 0 0 0 3 13l57.63 21.61l-14.62 73.25a8 8 0 0 0 13.69 7l112-120a8 8 0 0 0 1.94-7.26M109.37 214l10.47-52.38a8 8 0 0 0-5-9.06L62 132.71l84.62-90.66l-10.46 52.38a8 8 0 0 0 5 9.06l52.8 19.8Z"/></g></svg>'
title: Fast and easy queries
details: Easily get and store model data to your database from zig code.
---

View file

@ -1,12 +0,0 @@
{
"name": "zrm-docs",
"packageManager": "yarn@4.5.2",
"scripts": {
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
},
"devDependencies": {
"vitepress": "^1.5.0"
}
}

View file

@ -1 +0,0 @@
<svg width="512" height="512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" fill="white"><g style="display:inline;fill-opacity:1"><path d="M20 18c0 2.2091-3.5817 4-8 4-4.41828 0-8-1.7909-8-4v-4.026c.50221.6166 1.21495 1.1289 2.00774 1.5252C7.58004 16.2854 9.69967 16.75 12 16.75c2.3003 0 4.42-.4646 5.9923-1.2508.7928-.3963 1.5055-.9086 2.0077-1.5252Z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/><path d="M12 10.75c2.3003 0 4.42-.4646 5.9923-1.25075.7928-.3964 1.5055-.90866 2.0077-1.52528V12c0 .5-1.7857 1.5911-2.6786 2.1576C15.9983 14.8192 14.118 15.25 12 15.25c-2.11795 0-3.99832-.4308-5.32144-1.0924C5.5 13.5683 4 12.5 4 12V7.97397c.50221.61662 1.21495 1.12888 2.00774 1.52528C7.58004 10.2854 9.69967 10.75 12 10.75Z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/><path d="M17.3214 8.15761C15.9983 8.81917 14.118 9.25 12 9.25c-2.11795 0-3.99832-.43083-5.32144-1.09239-.51472-.20165-1.67219-.84269-2.47706-1.87826-.13696-.17622-.19574-.40082-.16162-.62137.02295-.14829.05492-.30103.0959-.39572C4.82815 3.40554 8.0858 2 12 2c3.9142 0 7.1718 1.40554 7.8642 3.26226.041.09469.073.24743.0959.39572.0341.22055-.0246.44515-.1616.62137-.8049 1.03557-1.9623 1.67661-2.4771 1.87826z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/></g><path style="display:inline;fill-opacity:1;stroke-width:23.5293" d="M276.04706 168.09412-.000001 474.65883l150.164711-43.03528 30.1891-25.96241L512 37.341172 362.30588 80.258809Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1 +0,0 @@
<svg width="512" height="512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g style="display:inline;fill-opacity:1"><path d="M20 18c0 2.2091-3.5817 4-8 4-4.41828 0-8-1.7909-8-4v-4.026c.50221.6166 1.21495 1.1289 2.00774 1.5252C7.58004 16.2854 9.69967 16.75 12 16.75c2.3003 0 4.42-.4646 5.9923-1.2508.7928-.3963 1.5055-.9086 2.0077-1.5252Z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/><path d="M12 10.75c2.3003 0 4.42-.4646 5.9923-1.25075.7928-.3964 1.5055-.90866 2.0077-1.52528V12c0 .5-1.7857 1.5911-2.6786 2.1576C15.9983 14.8192 14.118 15.25 12 15.25c-2.11795 0-3.99832-.4308-5.32144-1.0924C5.5 13.5683 4 12.5 4 12V7.97397c.50221.61662 1.21495 1.12888 2.00774 1.52528C7.58004 10.2854 9.69967 10.75 12 10.75Z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/><path d="M17.3214 8.15761C15.9983 8.81917 14.118 9.25 12 9.25c-2.11795 0-3.99832-.43083-5.32144-1.09239-.51472-.20165-1.67219-.84269-2.47706-1.87826-.13696-.17622-.19574-.40082-.16162-.62137.02295-.14829.05492-.30103.0959-.39572C4.82815 3.40554 8.0858 2 12 2c3.9142 0 7.1718 1.40554 7.8642 3.26226.041.09469.073.24743.0959.39572.0341.22055-.0246.44515-.1616.62137-.8049 1.03557-1.9623 1.67661-2.4771 1.87826z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/></g><path style="display:inline;fill-opacity:1;stroke-width:23.5293" d="M276.04706 168.09412-.000001 474.65883l150.164711-43.03528 30.1891-25.96241L512 37.341172 362.30588 80.258809Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,10 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 764 205.5" width="1528" height="411">
<!-- svg-source:excalidraw -->
<defs>
<style class="style-fonts">
@font-face { font-family: Comic Shanns; src: url(data:font/woff2;base64,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); }
</style>
</defs>
<rect x="0" y="0" width="764" height="205.5" fill="#ffffff"></rect><g stroke-linecap="round" transform="translate(10 10.5) rotate(0 144 92.5)"><path d="M-1.9 2.89 C107.45 2.5, 215.36 -0.37, 288.24 -1.34 M0.3 -1.46 C60.12 2.91, 118.57 1.74, 288.16 -0.34 M289.18 0.14 C283.94 72.43, 283.38 137.42, 287.33 183.94 M288.17 0.63 C291.55 57.13, 290.37 112.79, 289.04 184.41 M289.27 186.5 C182.41 187.24, 70.16 188.74, -0.96 186.3 M288.37 185.34 C200.27 180.6, 113.28 182.46, -0.39 185.05 M-0.45 187.47 C-0.47 133.34, -1.48 78.47, -1.75 3.41 M0.42 185.7 C1.02 111.53, 2.96 40.05, -1.35 1.65" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(123.20000076293945 50.5) rotate(0 30.799999237060547 52.5)"><text x="30.799999237060547" y="24.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">User</text><text x="30.799999237060547" y="59.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"></text><text x="30.799999237060547" y="94.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">id</text></g><g stroke-linecap="round" transform="translate(466 10) rotate(0 144 92.5)"><path d="M-1.91 -2.48 C61.07 -0.47, 118.05 0.32, 290.41 0.17 M-0.13 0.34 C87.86 -0.65, 173.95 -0.96, 288.19 -1.16 M288.41 1.95 C287.54 73.71, 287.93 147.22, 289.12 183.82 M289.11 -0.34 C290.62 53.43, 289.32 109.98, 289.38 186.73 M285.75 183.56 C196.57 186.45, 109.03 184.93, -1.97 184.69 M289.02 184.05 C191.11 188.42, 95.52 189.53, 0.53 186.34 M2.04 184.56 C-3.39 134.65, -0.79 80.46, -1.68 3.39 M1.42 186.98 C-1.58 112.68, -2.92 40.52, -0.86 -0.68" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(555.8 50) rotate(0 54.19999999999999 52.5)"><text x="54.199999999999996" y="24.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Message</text><text x="54.199999999999996" y="59.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"></text><text x="54.199999999999996" y="94.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">user_id</text></g><g stroke-linecap="round"><g transform="translate(303 135.5) rotate(0 79 0)"><path d="M0 0 C60.99 -4.14, 121.8 -0.06, 158 0 M0 0 C62.51 1.54, 121.81 -1.18, 158 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(303 135.5) rotate(0 79 0)"><path d="M135.05 7.74 C144.41 6.39, 153.93 3.3, 158.05 1.56 M134.98 8.6 C143.26 6.19, 151.68 1.44, 158.35 -0.98" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(303 135.5) rotate(0 79 0)"><path d="M135.15 -9.36 C144.59 -4.24, 154.08 -0.86, 158.05 1.56 M135.08 -8.5 C143.31 -4.4, 151.7 -2.64, 158.35 -0.98" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask></svg>

Before

Width:  |  Height:  |  Size: 7.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.9 KiB

View file

@ -1,10 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 765 206.5" width="1530" height="413">
<!-- svg-source:excalidraw -->
<defs>
<style class="style-fonts">
@font-face { font-family: Comic Shanns; src: url(data:font/woff2;base64,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); }
</style>
</defs>
<rect x="0" y="0" width="765" height="206.5" fill="#ffffff"></rect><g stroke-linecap="round" transform="translate(10 11.5) rotate(0 144 92.5)"><path d="M-1.9 2.89 C107.45 2.5, 215.36 -0.37, 288.24 -1.34 M0.3 -1.46 C60.12 2.91, 118.57 1.74, 288.16 -0.34 M289.18 0.14 C283.94 72.43, 283.38 137.42, 287.33 183.94 M288.17 0.63 C291.55 57.13, 290.37 112.79, 289.04 184.41 M289.27 186.5 C182.41 187.24, 70.16 188.74, -0.96 186.3 M288.37 185.34 C200.27 180.6, 113.28 182.46, -0.39 185.05 M-0.45 187.47 C-0.47 133.34, -1.48 78.47, -1.75 3.41 M0.42 185.7 C1.02 111.53, 2.96 40.05, -1.35 1.65" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(99.80000000000001 51.5) rotate(0 54.19999999999999 52.5)"><text x="54.199999999999996" y="24.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Message</text><text x="54.199999999999996" y="59.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"></text><text x="54.199999999999996" y="94.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">user_id</text></g><g stroke-linecap="round" transform="translate(467 10) rotate(0 144 92.5)"><path d="M-1.91 -2.48 C61.07 -0.47, 118.05 0.32, 290.41 0.17 M-0.13 0.34 C87.86 -0.65, 173.95 -0.96, 288.19 -1.16 M288.41 1.95 C287.54 73.71, 287.93 147.22, 289.12 183.82 M289.11 -0.34 C290.62 53.43, 289.32 109.98, 289.38 186.73 M285.75 183.56 C196.57 186.45, 109.03 184.93, -1.97 184.69 M289.02 184.05 C191.11 188.42, 95.52 189.53, 0.53 186.34 M2.04 184.56 C-3.39 134.65, -0.79 80.46, -1.68 3.39 M1.42 186.98 C-1.58 112.68, -2.92 40.52, -0.86 -0.68" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(580.2000007629395 50) rotate(0 30.799999237060547 52.5)"><text x="30.799999237060547" y="24.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">User</text><text x="30.799999237060547" y="59.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"></text><text x="30.799999237060547" y="94.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">id</text></g><g stroke-linecap="round"><g transform="translate(303 139.5) rotate(0 79.5 0)"><path d="M0 0 C56.02 -2.21, 117.58 -0.34, 159 0 M0 0 C35.29 -1.93, 74.18 -2.9, 159 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(303 139.5) rotate(0 79.5 0)"><path d="M134.16 6.09 C144.55 4.22, 151.3 0.71, 160.71 -0.86 M135.07 8.52 C143.72 5.45, 151.61 2.34, 158.53 -0.88" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(303 139.5) rotate(0 79.5 0)"><path d="M134.59 -11.01 C144.66 -6.61, 151.26 -3.84, 160.71 -0.86 M135.5 -8.58 C144.17 -5.85, 151.91 -3.14, 158.53 -0.88" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask></svg>

Before

Width:  |  Height:  |  Size: 7.3 KiB

View file

@ -1,10 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 765 206.5" width="1530" height="413">
<!-- svg-source:excalidraw -->
<defs>
<style class="style-fonts">
@font-face { font-family: Comic Shanns; src: url(data:font/woff2;base64,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); }
</style>
</defs>
<rect x="0" y="0" width="765" height="206.5" fill="#ffffff"></rect><g stroke-linecap="round" transform="translate(10 11.5) rotate(0 144 92.5)"><path d="M-1.9 2.89 C107.45 2.5, 215.36 -0.37, 288.24 -1.34 M0.3 -1.46 C60.12 2.91, 118.57 1.74, 288.16 -0.34 M289.18 0.14 C283.94 72.43, 283.38 137.42, 287.33 183.94 M288.17 0.63 C291.55 57.13, 290.37 112.79, 289.04 184.41 M289.27 186.5 C182.41 187.24, 70.16 188.74, -0.96 186.3 M288.37 185.34 C200.27 180.6, 113.28 182.46, -0.39 185.05 M-0.45 187.47 C-0.47 133.34, -1.48 78.47, -1.75 3.41 M0.42 185.7 C1.02 111.53, 2.96 40.05, -1.35 1.65" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(123.20000076293945 51.5) rotate(0 30.799999237060547 52.5)"><text x="30.799999237060547" y="24.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">User</text><text x="30.799999237060547" y="59.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"></text><text x="30.799999237060547" y="94.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">id</text></g><g stroke-linecap="round" transform="translate(467 10) rotate(0 144 92.5)"><path d="M-1.91 -2.48 C61.07 -0.47, 118.05 0.32, 290.41 0.17 M-0.13 0.34 C87.86 -0.65, 173.95 -0.96, 288.19 -1.16 M288.41 1.95 C287.54 73.71, 287.93 147.22, 289.12 183.82 M289.11 -0.34 C290.62 53.43, 289.32 109.98, 289.38 186.73 M285.75 183.56 C196.57 186.45, 109.03 184.93, -1.97 184.69 M289.02 184.05 C191.11 188.42, 95.52 189.53, 0.53 186.34 M2.04 184.56 C-3.39 134.65, -0.79 80.46, -1.68 3.39 M1.42 186.98 C-1.58 112.68, -2.92 40.52, -0.86 -0.68" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(549.4000015258789 50) rotate(0 61.599998474121094 52.5)"><text x="61.599998474121094" y="24.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">UserInfo</text><text x="61.599998474121094" y="59.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"></text><text x="61.599998474121094" y="94.5" font-family="Comic Shanns, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">user_id</text></g><g stroke-linecap="round"><g transform="translate(462 140.5) rotate(0 -79.5 0)"><path d="M0 0 C-54.59 2.6, -117.22 -0.35, -159 0 M0 0 C-52.69 -4.86, -106.46 -4.13, -159 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(462 140.5) rotate(0 -79.5 0)"><path d="M-134.7 -8.66 C-145.74 -6.56, -151.87 -1.4, -160.97 1.3 M-135.21 -9.29 C-140.81 -7.26, -145.18 -5.29, -158.97 -0.27" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(462 140.5) rotate(0 -79.5 0)"><path d="M-133.72 8.41 C-144.96 4.36, -151.45 3.37, -160.97 1.3 M-134.23 7.78 C-140.16 6.16, -144.74 4.48, -158.97 -0.27" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(608 68.5) rotate(0 0 0)"><path d="M0 0 C0 0, 0 0, 0 0 M0 0 C0 0, 0 0, 0 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(608 68.5) rotate(0 0 0)"><path d="MNaN NaN CNaN NaN, NaN NaN, NaN NaN MNaN NaN CNaN NaN, NaN NaN, NaN NaN" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(608 68.5) rotate(0 0 0)"><path d="MNaN NaN CNaN NaN, NaN NaN, NaN NaN MNaN NaN CNaN NaN, NaN NaN, NaN NaN" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask></svg>

Before

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="720.14001"
height="720.14001"
viewBox="0 0 190.53705 190.53705"
version="1.1"
id="svg1"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<linearGradient
id="linearGradient3">
<stop
style="stop-color:#22aeff;stop-opacity:1;"
offset="0"
id="stop1" />
<stop
style="stop-color:#00f1cb;stop-opacity:1;"
offset="1"
id="stop2" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient3"
id="linearGradient2"
x1="25.052734"
y1="160.9082"
x2="167.30664"
y2="12.158203"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#linearGradient3"
id="linearGradient6"
x1="215.58984"
y1="163.26172"
x2="42.798828"
y2="63.660156"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#linearGradient3"
id="linearGradient12"
x1="91.082031"
y1="73.657227"
x2="215.58984"
y2="73.657227"
gradientUnits="userSpaceOnUse" />
</defs>
<g
id="layer1"
transform="translate(-25.052762,8.7353212)">
<g
id="g1">
<path
id="path3-3"
style="display:inline;fill:url(#linearGradient12);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 176.85742,117.05273 H 103.875 L 91.082031,137.50977 H 215.58984 L 135.76758,9.8046875 h -25.93946 z" />
<path
id="path4"
style="display:inline;fill:url(#linearGradient6);stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 42.798828,163.26172 H 202.79688 l 12.79296,-20.45899 H 81.53125 L 118.02148,84.412109 105.05273,63.660156 Z" />
<path
id="path3"
style="display:inline;fill:url(#linearGradient2);stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 25.052734,140.15625 38.023438,160.9082 105.05273,53.669922 141.36719,111.75977 h 25.93945 L 105.05273,12.158203 Z" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because it is too large Load diff

View file

@ -1 +1,35 @@
<svg width="512" height="512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g style="display:inline;fill-opacity:1"><path d="M20 18c0 2.2091-3.5817 4-8 4-4.41828 0-8-1.7909-8-4v-4.026c.50221.6166 1.21495 1.1289 2.00774 1.5252C7.58004 16.2854 9.69967 16.75 12 16.75c2.3003 0 4.42-.4646 5.9923-1.2508.7928-.3963 1.5055-.9086 2.0077-1.5252Z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/><path d="M12 10.75c2.3003 0 4.42-.4646 5.9923-1.25075.7928-.3964 1.5055-.90866 2.0077-1.52528V12c0 .5-1.7857 1.5911-2.6786 2.1576C15.9983 14.8192 14.118 15.25 12 15.25c-2.11795 0-3.99832-.4308-5.32144-1.0924C5.5 13.5683 4 12.5 4 12V7.97397c.50221.61662 1.21495 1.12888 2.00774 1.52528C7.58004 10.2854 9.69967 10.75 12 10.75Z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/><path d="M17.3214 8.15761C15.9983 8.81917 14.118 9.25 12 9.25c-2.11795 0-3.99832-.43083-5.32144-1.09239-.51472-.20165-1.67219-.84269-2.47706-1.87826-.13696-.17622-.19574-.40082-.16162-.62137.02295-.14829.05492-.30103.0959-.39572C4.82815 3.40554 8.0858 2 12 2c3.9142 0 7.1718 1.40554 7.8642 3.26226.041.09469.073.24743.0959.39572.0341.22055-.0246.44515-.1616.62137-.8049 1.03557-1.9623 1.67661-2.4771 1.87826z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/></g><path style="display:inline;fill-opacity:1;stroke-width:23.5293" d="M276.04706 168.09412-.000001 474.65883l150.164711-43.03528 30.1891-25.96241L512 37.341172 362.30588 80.258809Z"/></svg> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"><g
style="display:inline;fill:#000000;fill-opacity:1"
id="g19"
transform="matrix(2.0189327,0,0,2.0189327,249.8064,248.00901)"><path
d="m 20,18 c 0,2.2091 -3.5817,4 -8,4 -4.41828,0 -8,-1.7909 -8,-4 v -4.026 c 0.50221,0.6166 1.21495,1.1289 2.00774,1.5252 C 7.58004,16.2854 9.69967,16.75 12,16.75 c 2.3003,0 4.42,-0.4646 5.9923,-1.2508 C 18.7851,15.1029 19.4978,14.5906 20,13.974 Z"
fill="#1c274c"
id="path17"
style="fill:#000000;fill-opacity:1"
transform="matrix(11.494213,0,0,11.494213,-134.80451,-133.9725)" /><path
d="m 12,10.75 c 2.3003,0 4.42,-0.4646 5.9923,-1.25075 C 18.7851,9.10285 19.4978,8.59059 20,7.97397 V 12 c 0,0.5 -1.7857,1.5911 -2.6786,2.1576 C 15.9983,14.8192 14.118,15.25 12,15.25 9.88205,15.25 8.00168,14.8192 6.67856,14.1576 5.5,13.5683 4,12.5 4,12 V 7.97397 C 4.50221,8.59059 5.21495,9.10285 6.00774,9.49925 7.58004,10.2854 9.69967,10.75 12,10.75 Z"
fill="#1c274c"
id="path18"
style="fill:#000000;fill-opacity:1"
transform="matrix(11.494213,0,0,11.494213,-134.80451,-133.9725)" /><path
d="M 17.3214,8.15761 C 15.9983,8.81917 14.118,9.25 12,9.25 9.88205,9.25 8.00168,8.81917 6.67856,8.15761 6.16384,7.95596 5.00637,7.31492 4.2015,6.27935 4.06454,6.10313 4.00576,5.87853 4.03988,5.65798 4.06283,5.50969 4.0948,5.35695 4.13578,5.26226 4.82815,3.40554 8.0858,2 12,2 c 3.9142,0 7.1718,1.40554 7.8642,3.26226 0.041,0.09469 0.073,0.24743 0.0959,0.39572 0.0341,0.22055 -0.0246,0.44515 -0.1616,0.62137 -0.8049,1.03557 -1.9623,1.67661 -2.4771,1.87826 z"
fill="#1c274c"
id="path19"
style="fill:#000000;fill-opacity:1"
transform="matrix(11.494213,0,0,11.494213,-134.80451,-133.9725)" /></g><path
id="polygon3"
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:23.5293"
d="M 276.04706,168.09412 -10e-7,474.65883 150.16471,431.62355 180.35381,405.66114 512,37.341172 362.30588,80.258809 Z" /></g></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,33 +0,0 @@
/// Append an element to the given array at comptime.
pub fn append(array: anytype, element: anytype) @TypeOf(array ++ .{element}) {
return array ++ .{element};
}
/// Join strings into one, with the given separator in between.
pub fn join(separator: []const u8, slices: []const[]const u8) []const u8 {
if (slices.len == 0) return "";
// Compute total length of the string to make.
const totalLen = total: {
// Compute separator length.
var total = separator.len * (slices.len - 1);
// Add length of all slices.
for (slices) |slice| total += slice.len;
break :total total;
};
var buffer: [totalLen]u8 = undefined;
// Based on std.mem.joinMaybeZ implementation.
@memcpy(buffer[0..slices[0].len], slices[0]);
var buf_index: usize = slices[0].len;
for (slices[1..]) |slice| {
@memcpy(buffer[buf_index .. buf_index + separator.len], separator);
buf_index += separator.len;
@memcpy(buffer[buf_index .. buf_index + slice.len], slice);
buf_index += slice.len;
}
return &buffer;
}

View file

@ -1,11 +1,11 @@
const std = @import("std"); const std = @import("std");
const _sql = @import("sql.zig"); const _sql = @import("sql.zig");
const ZrmError = @import("errors.zig").ZrmError; const errors = @import("errors.zig");
const Static = @This(); const Static = @This();
/// Create a value condition on a column. /// Create a value condition on a column.
pub fn value(comptime ValueType: type, allocator: std.mem.Allocator, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !_sql.RawQuery { pub fn value(comptime ValueType: type, allocator: std.mem.Allocator, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !_sql.SqlParams {
// Initialize the SQL condition string. // Initialize the SQL condition string.
var comptimeSql: [_column.len + 1 + operator.len + 1 + 1]u8 = undefined; var comptimeSql: [_column.len + 1 + operator.len + 1 + 1]u8 = undefined;
@memcpy(comptimeSql[0.._column.len], _column); @memcpy(comptimeSql[0.._column.len], _column);
@ -18,8 +18,8 @@ pub fn value(comptime ValueType: type, allocator: std.mem.Allocator, comptime _c
std.mem.copyForwards(u8, sqlBuf, &comptimeSql); std.mem.copyForwards(u8, sqlBuf, &comptimeSql);
// Initialize parameters array. // Initialize parameters array.
const params = try allocator.alloc(_sql.RawQueryParameter, 1); const params = try allocator.alloc(_sql.QueryParameter, 1);
params[0] = try _sql.RawQueryParameter.fromValue(_value); params[0] = try _sql.QueryParameter.fromValue(_value);
// Return the built SQL condition. // Return the built SQL condition.
return .{ return .{
@ -29,7 +29,7 @@ pub fn value(comptime ValueType: type, allocator: std.mem.Allocator, comptime _c
} }
/// Create a column condition on a column. /// Create a column condition on a column.
pub fn column(allocator: std.mem.Allocator, comptime _column: []const u8, comptime operator: []const u8, comptime valueColumn: []const u8) !_sql.RawQuery { pub fn column(allocator: std.mem.Allocator, comptime _column: []const u8, comptime operator: []const u8, comptime valueColumn: []const u8) !_sql.SqlParams {
// Initialize the SQL condition string. // Initialize the SQL condition string.
var comptimeSql: [_column.len + 1 + operator.len + 1 + valueColumn.len]u8 = undefined; var comptimeSql: [_column.len + 1 + operator.len + 1 + valueColumn.len]u8 = undefined;
@memcpy(comptimeSql[0.._column.len], _column); @memcpy(comptimeSql[0.._column.len], _column);
@ -45,19 +45,19 @@ pub fn column(allocator: std.mem.Allocator, comptime _column: []const u8, compti
// Return the built SQL condition. // Return the built SQL condition.
return .{ return .{
.sql = sqlBuf, .sql = sqlBuf,
.params = &[0]_sql.RawQueryParameter{}, .params = &[0]_sql.QueryParameter{},
}; };
} }
/// Create an IN condition on a column. /// Create an IN condition on a column.
pub fn in(comptime ValueType: type, allocator: std.mem.Allocator, _column: []const u8, _value: []const ValueType) !_sql.RawQuery { pub fn in(comptime ValueType: type, allocator: std.mem.Allocator, _column: []const u8, _value: []const ValueType) !_sql.SqlParams {
// Generate parameters SQL. // Generate parameters SQL.
const parametersSql = try _sql.generateParametersSql(allocator, _value.len); const parametersSql = try _sql.generateParametersSql(allocator, _value.len);
// Get all query parameters from given values. // Get all query parameters from given values.
var valueParameters: []_sql.RawQueryParameter = try allocator.alloc(_sql.RawQueryParameter, _value.len); var valueParameters: []_sql.QueryParameter = try allocator.alloc(_sql.QueryParameter, _value.len);
for (0.._value.len) |i| { for (0.._value.len) |i| {
// Convert every given value to a query parameter. // Convert every given value to a query parameter.
valueParameters[i] = try _sql.RawQueryParameter.fromValue(_value[i]); valueParameters[i] = try _sql.QueryParameter.fromValue(_value[i]);
} }
// Initialize the SQL condition string. // Initialize the SQL condition string.
@ -75,10 +75,10 @@ pub fn in(comptime ValueType: type, allocator: std.mem.Allocator, _column: []con
} }
/// Generic conditions combiner generator. /// Generic conditions combiner generator.
fn conditionsCombiner(comptime keyword: []const u8, allocator: std.mem.Allocator, subconditions: []const _sql.RawQuery) !_sql.RawQuery { fn conditionsCombiner(comptime keyword: []const u8, allocator: std.mem.Allocator, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
if (subconditions.len == 0) { if (subconditions.len == 0) {
// At least one condition is required. // At least one condition is required.
return ZrmError.AtLeastOneConditionRequired; return errors.ZrmError.AtLeastOneConditionRequired;
} }
// Full keyword constant. // Full keyword constant.
@ -97,7 +97,7 @@ fn conditionsCombiner(comptime keyword: []const u8, allocator: std.mem.Allocator
// Initialize the SQL condition string. // Initialize the SQL condition string.
var sqlBuf = try allocator.alloc(u8, sqlSize); var sqlBuf = try allocator.alloc(u8, sqlSize);
// Initialize the query parameters array. // Initialize the query parameters array.
var parameters = try allocator.alloc(_sql.RawQueryParameter, queryParametersCount); var parameters = try allocator.alloc(_sql.QueryParameter, queryParametersCount);
var sqlBufCursor: usize = 0; var parametersCursor: usize = 0; var sqlBufCursor: usize = 0; var parametersCursor: usize = 0;
// Add first parenthesis. // Add first parenthesis.
@ -117,7 +117,7 @@ fn conditionsCombiner(comptime keyword: []const u8, allocator: std.mem.Allocator
} }
// Add query parameters to the array. // Add query parameters to the array.
std.mem.copyForwards(_sql.RawQueryParameter, parameters[parametersCursor..parametersCursor+subcondition.params.len], subcondition.params); std.mem.copyForwards(_sql.QueryParameter, parameters[parametersCursor..parametersCursor+subcondition.params.len], subcondition.params);
parametersCursor += subcondition.params.len; parametersCursor += subcondition.params.len;
} }
@ -132,12 +132,12 @@ fn conditionsCombiner(comptime keyword: []const u8, allocator: std.mem.Allocator
} }
/// Create an AND condition between multiple sub-conditions. /// Create an AND condition between multiple sub-conditions.
pub fn @"and"(allocator: std.mem.Allocator, subconditions: []const _sql.RawQuery) !_sql.RawQuery { pub fn @"and"(allocator: std.mem.Allocator, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
return conditionsCombiner("AND", allocator, subconditions); return conditionsCombiner("AND", allocator, subconditions);
} }
/// Create an OR condition between multiple sub-conditions. /// Create an OR condition between multiple sub-conditions.
pub fn @"or"(allocator: std.mem.Allocator, subconditions: []const _sql.RawQuery) !_sql.RawQuery { pub fn @"or"(allocator: std.mem.Allocator, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
return conditionsCombiner("OR", allocator, subconditions); return conditionsCombiner("OR", allocator, subconditions);
} }
@ -148,27 +148,27 @@ pub const Builder = struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
/// Create a value condition on a column. /// Create a value condition on a column.
pub fn value(self: Self, comptime ValueType: type, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !_sql.RawQuery { pub fn value(self: Self, comptime ValueType: type, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !_sql.SqlParams {
return Static.value(ValueType, self.allocator, _column, operator, _value); return Static.value(ValueType, self.allocator, _column, operator, _value);
} }
/// Create a column condition on a column. /// Create a column condition on a column.
pub fn column(self: Self, comptime _column: []const u8, comptime operator: []const u8, comptime valueColumn: []const u8) !_sql.RawQuery { pub fn column(self: Self, comptime _column: []const u8, comptime operator: []const u8, comptime valueColumn: []const u8) !_sql.SqlParams {
return Static.column(self.allocator, _column, operator, valueColumn); return Static.column(self.allocator, _column, operator, valueColumn);
} }
/// Create an IN condition on a column. /// Create an IN condition on a column.
pub fn in(self: Self, comptime ValueType: type, _column: []const u8, _value: []const ValueType) !_sql.RawQuery { pub fn in(self: Self, comptime ValueType: type, _column: []const u8, _value: []const ValueType) !_sql.SqlParams {
return Static.in(ValueType, self.allocator, _column, _value); return Static.in(ValueType, self.allocator, _column, _value);
} }
/// Create an AND condition between multiple sub-conditions. /// Create an AND condition between multiple sub-conditions.
pub fn @"and"(self: Self, subconditions: []const _sql.RawQuery) !_sql.RawQuery { pub fn @"and"(self: Self, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
return Static.@"and"(self.allocator, subconditions); return Static.@"and"(self.allocator, subconditions);
} }
/// Create an OR condition between multiple sub-conditions. /// Create an OR condition between multiple sub-conditions.
pub fn @"or"(self: Self, subconditions: []const _sql.RawQuery) !_sql.RawQuery { pub fn @"or"(self: Self, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
return Static.@"or"(self.allocator, subconditions); return Static.@"or"(self.allocator, subconditions);
} }

View file

@ -1,109 +0,0 @@
const std = @import("std");
const pg = @import("pg");
const session = @import("session.zig");
/// Abstract connection, provided by a connector.
pub const Connection = struct {
/// Raw connection.
connection: *pg.Conn,
/// Connection implementation.
_interface: struct {
instance: *anyopaque,
release: *const fn (self: *Connection) void,
},
/// Release the connection.
pub fn release(self: *Connection) void {
self._interface.release(self);
}
};
/// Database connection manager for queries.
pub const Connector = struct {
const Self = @This();
/// Internal interface structure.
_interface: struct {
instance: *anyopaque,
getConnection: *const fn (self: *anyopaque) anyerror!*Connection,
},
/// Get a connection.
pub fn getConnection(self: Self) !*Connection {
return try self._interface.getConnection(self._interface.instance);
}
};
/// A simple pool connection.
pub const PoolConnection = struct {
const Self = @This();
/// Connector of the connection.
connector: *PoolConnector,
/// Connection instance, to only keep one at a time.
_connection: ?Connection = null,
/// Get a database connection.
pub fn connection(self: *Self) !*Connection {
if (self._connection == null) {
// A new connection needs to be initialized.
self._connection = .{
.connection = try self.connector.pool.acquire(),
._interface = .{
.instance = self,
.release = releaseConnection,
},
};
}
// Return the initialized connection.
return &(self._connection.?);
}
// Implementation.
/// Release the pool connection.
fn releaseConnection(self: *Connection) void {
self.connection.release();
// Free allocated connection.
const poolConnection: *PoolConnection = @ptrCast(@alignCast(self._interface.instance));
poolConnection.connector.pool._allocator.destroy(poolConnection);
}
};
/// A simple pool connector.
pub const PoolConnector = struct {
const Self = @This();
pool: *pg.Pool,
/// Get a database connector instance for the current pool.
pub fn connector(self: *Self) Connector {
return .{
._interface = .{
.instance = self,
.getConnection = getConnection,
},
};
}
// Implementation.
/// Get the connection from the pool.
fn getConnection(opaqueSelf: *anyopaque) !*Connection {
const self: *Self = @ptrCast(@alignCast(opaqueSelf));
// Initialize a new connection.
const poolConnection = try self.pool._allocator.create(PoolConnection);
poolConnection.* = .{
.connector = self,
};
// Acquire a new connection from the pool.
return try poolConnection.connection();
}
};

View file

@ -1,29 +0,0 @@
const std = @import("std");
/// Simple ModelFromSql and ModelToSql functions for models which have the same table definition.
pub fn TableModel(comptime Model: type, comptime TableShape: type) type {
// Get fields of the model, which must be the same as the table shape.
const fields = std.meta.fields(TableShape);
return struct {
/// Simply copy all fields from model to table.
pub fn copyModelToTable(_model: Model) !TableShape {
var _table: TableShape = undefined;
inline for (fields) |modelField| {
// Copy each field of the model to the table.
@field(_table, modelField.name) = @field(_model, modelField.name);
}
return _table;
}
/// Simply copy all fields from table to model.
pub fn copyTableToModel(_table: TableShape) !Model {
var _model: Model = undefined;
inline for (fields) |tableField| {
// Copy each field of the table to the model.
@field(_model, tableField.name) = @field(_table, tableField.name);
}
return _model;
}
};
}

View file

@ -1,56 +1,24 @@
const std = @import("std"); const std = @import("std");
const pg = @import("pg"); const pg = @import("pg");
const zollections = @import("zollections"); const zollections = @import("zollections");
const ZrmError = @import("errors.zig").ZrmError; const errors = @import("errors.zig");
const database = @import("database.zig");
const postgresql = @import("postgresql.zig"); const postgresql = @import("postgresql.zig");
const _sql = @import("sql.zig"); const _sql = @import("sql.zig");
const repository = @import("repository.zig"); const repository = @import("repository.zig");
const _result = @import("result.zig");
/// Type of an insertable column. Insert shape should be composed of only these. /// Type of an insertable column. Insert shape should be composed of only these.
fn InsertableColumn(comptime ValueType: type) type { pub fn Insertable(comptime ValueType: type) type {
return struct { return struct {
value: ?ValueType = null, value: ?ValueType = null,
default: bool = false, default: bool = false,
}; };
} }
/// Build an insertable structure type from a normal structure.
pub fn Insertable(comptime StructType: type) type {
// Get type info of the given structure.
const typeInfo = @typeInfo(StructType);
// Initialize fields of the insertable struct.
var newFields: [typeInfo.Struct.fields.len]std.builtin.Type.StructField = undefined;
for (typeInfo.Struct.fields, &newFields) |field, *newField| {
// Create a new field for each field of the given struct.
const newFieldType = InsertableColumn(field.type);
newField.* = std.builtin.Type.StructField{
.name = field.name,
.type = newFieldType,
.default_value = null,
.is_comptime = false,
.alignment = @alignOf(newFieldType),
};
}
// Return the insertable structure type.
return @Type(std.builtin.Type{
.Struct = .{
.layout = .auto,
.decls = &[0]std.builtin.Type.Declaration{},
.fields = &newFields,
.is_tuple = false,
},
});
}
/// Repository insert query configuration structure. /// Repository insert query configuration structure.
pub fn RepositoryInsertConfiguration(comptime InsertShape: type) type { pub fn RepositoryInsertConfiguration(comptime InsertShape: type) type {
return struct { return struct {
values: []const Insertable(InsertShape) = undefined, values: []const InsertShape = undefined,
returning: ?_sql.RawQuery = null, returning: ?_sql.SqlParams = null,
}; };
} }
@ -119,18 +87,14 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
const Configuration = RepositoryInsertConfiguration(InsertShape); const Configuration = RepositoryInsertConfiguration(InsertShape);
/// Result mapper type.
pub const ResultMapper = _result.ResultMapper(Model, TableShape, null, repositoryConfig, null, null);
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
connector: database.Connector, database: *pg.Pool,
connection: *database.Connection = undefined,
insertConfig: Configuration, insertConfig: Configuration,
sql: ?[]const u8 = null, sql: ?[]const u8 = null,
/// Parse given model or shape and put the result in newValue. /// Parse given model or shape and put the result in newValue.
fn parseData(newValue: *Insertable(InsertShape), value: anytype) !void { fn parseData(newValue: *InsertShape, value: anytype) !void {
// If the given value is a model, first convert it to its SQL equivalent. // If the given value is a model, first convert it to its SQL equivalent.
if (@TypeOf(value) == Model) { if (@TypeOf(value) == Model) {
return parseData(newValue, try repositoryConfig.toSql(value)); return parseData(newValue, try repositoryConfig.toSql(value));
@ -143,7 +107,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
/// Parse one value to insert. /// Parse one value to insert.
fn parseOne(self: *Self, value: anytype) !void { fn parseOne(self: *Self, value: anytype) !void {
const newValues = try self.arena.allocator().alloc(Insertable(InsertShape), 1); const newValues = try self.arena.allocator().alloc(InsertShape, 1);
// Parse the given value. // Parse the given value.
try parseData(&newValues[0], value); try parseData(&newValues[0], value);
self.insertConfig.values = newValues; self.insertConfig.values = newValues;
@ -151,7 +115,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
/// Parse a slice of values to insert. /// Parse a slice of values to insert.
fn parseSlice(self: *Self, value: anytype) !void { fn parseSlice(self: *Self, value: anytype) !void {
const newValues = try self.arena.allocator().alloc(Insertable(InsertShape), value.len); const newValues = try self.arena.allocator().alloc(InsertShape, value.len);
for (0..value.len) |i| { for (0..value.len) |i| {
// Parse each value in the given slice. // Parse each value in the given slice.
try parseData(&newValues[i], value[i]); try parseData(&newValues[i], value[i]);
@ -191,20 +155,20 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
} }
/// Set selected columns for RETURNING clause. /// Set selected columns for RETURNING clause.
pub fn returning(self: *Self, _select: _sql.RawQuery) void { pub fn returning(self: *Self, _select: _sql.SqlParams) void {
self.insertConfig.returning = _select; self.insertConfig.returning = _select;
} }
/// Set selected columns for RETURNING clause. /// Set selected columns for RETURNING clause.
pub fn returningColumns(self: *Self, _select: []const []const u8) !void { pub fn returningColumns(self: *Self, _select: []const []const u8) void {
if (_select.len == 0) { if (_select.len == 0) {
return ZrmError.AtLeastOneSelectionRequired; return errors.AtLeastOneSelectionRequired;
} }
self.returning(.{ self.returning(.{
// Join selected columns. // Join selected columns.
.sql = try std.mem.join(self.arena.allocator(), ", ", _select), .sql = std.mem.join(self.arena.allocator(), ", ", _select),
.params = &[_]_sql.RawQueryParameter{}, // No parameters. .params = &[_]_sql.QueryParameter{}, // No parameters.
}); });
} }
@ -212,7 +176,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
pub fn returningAll(self: *Self) void { pub fn returningAll(self: *Self) void {
self.returning(.{ self.returning(.{
.sql = "*", .sql = "*",
.params = &[_]_sql.RawQueryParameter{}, // No parameters. .params = &[_]_sql.QueryParameter{}, // No parameters.
}); });
} }
@ -220,7 +184,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
pub fn buildSql(self: *Self) !void { pub fn buildSql(self: *Self) !void {
if (self.insertConfig.values.len == 0) { if (self.insertConfig.values.len == 0) {
// At least one value is required to insert. // At least one value is required to insert.
return ZrmError.AtLeastOneValueRequired; return errors.ZrmError.AtLeastOneValueRequired;
} }
// Compute VALUES parameters count. // Compute VALUES parameters count.
@ -240,71 +204,75 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
) else 0; ) else 0;
// Initialize SQL buffer. // Initialize SQL buffer.
var sqlBuf = try std.ArrayList(u8).initCapacity(self.arena.allocator(), fixedSqlSize + valuesSqlSize + returningSize); const sqlBuf = try self.arena.allocator().alloc(u8, fixedSqlSize + valuesSqlSize + returningSize);
defer sqlBuf.deinit();
// Append initial "INSERT INTO table VALUES ". // Append initial "INSERT INTO table VALUES ".
try sqlBuf.appendSlice(sqlBase); @memcpy(sqlBuf[0..sqlBase.len],sqlBase);
var sqlBufCursor: usize = sqlBase.len;
// Start parameter counter at 1. // Start parameter counter at 1.
var currentParameter: usize = 1; var currentParameter: usize = 1;
if (self.insertConfig.values.len == 0) { if (self.insertConfig.values.len == 0) {
// No values, output an empty values set. // No values, output an empty values set.
try sqlBuf.appendSlice("()"); std.mem.copyForwards(u8, sqlBuf[sqlBufCursor..sqlBufCursor+2], "()");
sqlBufCursor += 2;
} else { } else {
// Build values set. // Build values set.
for (self.insertConfig.values) |_| { for (self.insertConfig.values) |_| {
// Add the first '('. // Add the first '('.
try sqlBuf.append('('); sqlBuf[sqlBufCursor] = '('; sqlBufCursor += 1;
inline for (columns) |_| { inline for (columns) |_| {
// Create the parameter string and append it to the SQL buffer. // Create the parameter string and append it to the SQL buffer.
try sqlBuf.writer().print("${d},", .{currentParameter}); const paramSize = 1 + try _sql.computeRequiredSpaceForParameter(currentParameter) + 1;
_ = try std.fmt.bufPrint(sqlBuf[sqlBufCursor..sqlBufCursor+paramSize], "${d},", .{currentParameter});
sqlBufCursor += paramSize;
// Increment parameter count. // Increment parameter count.
currentParameter += 1; currentParameter += 1;
} }
// Replace the final ',' with a ')'. // Replace the final ',' with a ')'.
sqlBuf.items[sqlBuf.items.len - 1] = ')'; sqlBuf[sqlBufCursor - 1] = ')';
// Add the final ','. // Add the final ','.
try sqlBuf.append(','); sqlBuf[sqlBufCursor] = ','; sqlBufCursor += 1;
} }
sqlBufCursor -= 1;
// Remove the last ','.
_ = sqlBuf.pop();
} }
// Append RETURNING clause, if there is one defined. // Append RETURNING clause, if there is one defined.
if (self.insertConfig.returning) |_returning| { if (self.insertConfig.returning) |_returning| {
try sqlBuf.appendSlice(" " ++ returningClause ++ " "); @memcpy(sqlBuf[sqlBufCursor..sqlBufCursor+(1 + returningClause.len + 1)], " " ++ returningClause ++ " ");
// Copy RETURNING clause content and replace parameters, if there are some. // Copy RETURNING clause content and replace parameters, if there are some.
try _sql.copyAndReplaceSqlParameters(&currentParameter, try _sql.copyAndReplaceSqlParameters(&currentParameter,
_returning.params.len, sqlBuf.writer(), _returning.sql _returning.params.len,
sqlBuf[sqlBufCursor+(1+returningClause.len+1)..sqlBufCursor+returningSize], _returning.sql
); );
sqlBufCursor += returningSize;
} }
// ";" to end the query. // ";" to end the query.
try sqlBuf.append(';'); sqlBuf[sqlBufCursor] = ';'; sqlBufCursor += 1;
// Save built SQL query. // Save built SQL query.
self.sql = try sqlBuf.toOwnedSlice(); self.sql = sqlBuf;
} }
/// Execute the insert query. /// Execute the insert query.
fn execQuery(self: *Self) !*pg.Result { fn execQuery(self: *Self) !*pg.Result {
// Get a connection to the database. // Get a connection to the database.
self.connection = try self.connector.getConnection(); const connection = try self.database.acquire();
errdefer self.connection.release(); errdefer connection.release();
// Initialize a new PostgreSQL statement. // Initialize a new PostgreSQL statement.
var statement = try pg.Stmt.init(self.connection.connection, .{ var statement = try pg.Stmt.init(connection, .{
.column_names = true, .column_names = true,
.release_conn = true,
.allocator = self.arena.allocator(), .allocator = self.arena.allocator(),
}); });
errdefer statement.deinit(); errdefer statement.deinit();
// Prepare SQL insert query. // Prepare SQL insert query.
statement.prepare(self.sql.?) statement.prepare(self.sql.?)
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement); catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
// Bind INSERT query parameters. // Bind INSERT query parameters.
for (self.insertConfig.values) |row| { for (self.insertConfig.values) |row| {
@ -319,7 +287,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
// Execute the query and get its result. // Execute the query and get its result.
const result = statement.execute() const result = statement.execute()
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement); catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
// Query executed successfully, return the result. // Query executed successfully, return the result.
return result; return result;
@ -332,20 +300,40 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
// Execute query and get its result. // Execute query and get its result.
var queryResult = try self.execQuery(); var queryResult = try self.execQuery();
defer self.connection.release();
defer queryResult.deinit(); defer queryResult.deinit();
// Map query results. //TODO deduplicate this in postgresql.zig, we could do it if Mapper type was exposed.
var postgresqlReader = postgresql.QueryResultReader(TableShape, null, null).init(queryResult); //TODO make a generic mapper and do it in repository.zig?
return try ResultMapper.map(false, allocator, self.connector, postgresqlReader.reader()); // Create an arena for mapper data.
var mapperArena = std.heap.ArenaAllocator.init(allocator);
// Get result mapper.
const mapper = queryResult.mapper(TableShape, .{ .allocator = mapperArena.allocator() });
// Initialize models list.
var models = std.ArrayList(*Model).init(allocator);
defer models.deinit();
// Get all raw models from the result mapper.
while (try mapper.next()) |rawModel| {
// Parse each raw model from the mapper.
const model = try allocator.create(Model);
model.* = try repositoryConfig.fromSql(rawModel);
try models.append(model);
}
// Return a result with the models.
return repository.RepositoryResult(Model).init(allocator,
zollections.Collection(Model).init(allocator, try models.toOwnedSlice()),
mapperArena,
);
} }
/// Initialize a new repository insert query. /// Initialize a new repository insert query.
pub fn init(allocator: std.mem.Allocator, connector: database.Connector) Self { pub fn init(allocator: std.mem.Allocator, database: *pg.Pool) Self {
return .{ return .{
// Initialize an arena allocator for the insert query. // Initialize an arena allocator for the insert query.
.arena = std.heap.ArenaAllocator.init(allocator), .arena = std.heap.ArenaAllocator.init(allocator),
.connector = connector, .database = database,
.insertConfig = .{}, .insertConfig = .{},
}; };
} }

View file

@ -1,13 +1,9 @@
const std = @import("std"); const std = @import("std");
const pg = @import("pg"); const pg = @import("pg");
const zollections = @import("zollections");
const global = @import("global.zig"); const global = @import("global.zig");
const ZrmError = @import("errors.zig").ZrmError; const errors = @import("errors.zig");
const database = @import("database.zig");
const _sql = @import("sql.zig"); const _sql = @import("sql.zig");
const _relationships = @import("relationships.zig");
const repository = @import("repository.zig"); const repository = @import("repository.zig");
const _result = @import("result.zig");
/// PostgreSQL query error details. /// PostgreSQL query error details.
pub const PostgresqlError = struct { pub const PostgresqlError = struct {
@ -16,7 +12,7 @@ pub const PostgresqlError = struct {
}; };
/// Try to bind query parameters to the statement. /// Try to bind query parameters to the statement.
pub fn bindQueryParameters(statement: *pg.Stmt, parameters: []const _sql.RawQueryParameter) !void { pub fn bindQueryParameters(statement: *pg.Stmt, parameters: []const _sql.QueryParameter) !void {
for (parameters) |parameter| { for (parameters) |parameter| {
// Try to bind each parameter in the slice. // Try to bind each parameter in the slice.
try bindQueryParameter(statement, parameter); try bindQueryParameter(statement, parameter);
@ -24,7 +20,7 @@ pub fn bindQueryParameters(statement: *pg.Stmt, parameters: []const _sql.RawQuer
} }
/// Try to bind a query parameter to the statement. /// Try to bind a query parameter to the statement.
pub fn bindQueryParameter(statement: *pg.Stmt, parameter: _sql.RawQueryParameter) !void { pub fn bindQueryParameter(statement: *pg.Stmt, parameter: _sql.QueryParameter) !void {
switch (parameter) { switch (parameter) {
.integer => |integer| try statement.bind(integer), .integer => |integer| try statement.bind(integer),
.number => |number| try statement.bind(number), .number => |number| try statement.bind(number),
@ -35,15 +31,11 @@ pub fn bindQueryParameter(statement: *pg.Stmt, parameter: _sql.RawQueryParameter
} }
/// PostgreSQL error handling by ZRM. /// PostgreSQL error handling by ZRM.
pub fn handlePostgresqlError(err: anyerror, connection: *database.Connection, statement: *pg.Stmt) anyerror { pub fn handlePostgresqlError(err: anyerror, connection: *pg.Conn, statement: *pg.Stmt) anyerror {
// Release connection and statement as query failed. // Release connection and statement as query failed.
defer statement.deinit(); defer statement.deinit();
defer connection.release();
return handleRawPostgresqlError(err, connection.connection);
}
/// PostgreSQL raw error handling by ZRM.
pub fn handleRawPostgresqlError(err: anyerror, connection: *pg.Conn) anyerror {
if (connection.err) |sqlErr| { if (connection.err) |sqlErr| {
if (global.debugMode) { if (global.debugMode) {
// If debug mode is enabled, show the PostgreSQL error. // If debug mode is enabled, show the PostgreSQL error.
@ -51,304 +43,9 @@ pub fn handleRawPostgresqlError(err: anyerror, connection: *pg.Conn) anyerror {
} }
// Return that an error happened in query execution. // Return that an error happened in query execution.
return ZrmError.QueryFailed; return errors.ZrmError.QueryFailed;
} else { } else {
// Not an SQL error, just return it. // Not an SQL error, just return it.
return err; return err;
} }
} }
const PgError = error {
NullValue,
};
fn isSlice(comptime T: type) ?type {
switch(@typeInfo(T)) {
.Pointer => |ptr| {
if (ptr.size != .Slice) {
@compileError("cannot get value of type " ++ @typeName(T));
}
return if (ptr.child == u8) null else ptr.child;
},
.Optional => |opt| return isSlice(opt.child),
else => return null,
}
}
fn mapValue(comptime T: type, value: T, allocator: std.mem.Allocator) !T {
switch (@typeInfo(T)) {
.Optional => |opt| {
if (value) |v| {
return try mapValue(opt.child, v, allocator);
}
return null;
},
else => {},
}
if (T == []u8 or T == []const u8) {
return try allocator.dupe(u8, value);
}
if (std.meta.hasFn(T, "pgzMoveOwner")) {
return value.pgzMoveOwner(allocator);
}
return value;
}
fn getScalar(T: type, data: []const u8, oid: i32) T {
switch (T) {
u8 => return pg.types.Char.decode(data, oid),
i16 => return pg.types.Int16.decode(data, oid),
i32 => return pg.types.Int32.decode(data, oid),
i64 => return pg.types.Int64.decode(data, oid),
f32 => return pg.types.Float32.decode(data, oid),
f64 => return pg.types.Float64.decode(data, oid),
bool => return pg.types.Bool.decode(data, oid),
[]const u8 => return pg.types.Bytea.decode(data, oid),
[]u8 => return @constCast(pg.types.Bytea.decode(data, oid)),
pg.types.Numeric => return pg.types.Numeric.decode(data, oid),
pg.types.Cidr => return pg.types.Cidr.decode(data, oid),
else => switch (@typeInfo(T)) {
.Enum => {
const str = pg.types.Bytea.decode(data, oid);
return std.meta.stringToEnum(T, str).?;
},
else => @compileError("cannot get value of type " ++ @typeName(T)),
},
}
}
pub fn rowGet(self: *const pg.Row, comptime T: type, col: usize) PgError!T {
const value = self.values[col];
const TT = switch (@typeInfo(T)) {
.Optional => |opt| {
if (value.is_null) {
return null;
} else {
return self.get(opt.child, col);
}
},
.Struct => blk: {
if (@hasDecl(T, "fromPgzRow") == true) {
return T.fromPgzRow(value, self.oids[col]);
}
break :blk T;
},
else => blk: {
if (value.is_null) return PgError.NullValue;
break :blk T;
},
};
return getScalar(TT, value.data, self.oids[col]);
}
fn rowMapColumn(self: *const pg.Row, field: *const std.builtin.Type.StructField, optional_column_index: ?usize, allocator: ?std.mem.Allocator) !field.type {
const T = field.type;
const column_index = optional_column_index orelse {
if (field.default_value) |dflt| {
return @as(*align(1) const field.type, @ptrCast(dflt)).*;
}
return error.FieldColumnMismatch;
};
if (comptime isSlice(T)) |S| {
const slice = blk: {
if (@typeInfo(T) == .Optional) {
break :blk try rowGet(self, ?pg.Iterator(S), column_index) orelse return null;
} else {
break :blk try rowGet(self, pg.Iterator(S), column_index);
}
};
return try slice.alloc(allocator orelse return error.AllocatorRequiredForSliceMapping);
}
const value = try rowGet(self, field.type, column_index);
const a = allocator orelse return value;
return mapValue(T, value, a);
}
pub fn PgMapper(comptime T: type) type {
return struct {
result: *pg.Result,
allocator: ?std.mem.Allocator,
column_indexes: [std.meta.fields(T).len]?usize,
const Self = @This();
pub fn next(self: *const Self, row: *pg.Row) !?T {
var value: T = undefined;
const allocator = self.allocator;
inline for (std.meta.fields(T), self.column_indexes) |field, optional_column_index| {
//TODO I must reimplement row.mapColumn because it's not public :-(
@field(value, field.name) = try rowMapColumn(row, &field, optional_column_index, allocator);
}
return value;
}
};
}
/// Make a PostgreSQL result mapper with the given prefix, if there is one.
pub fn makeMapper(comptime T: type, result: *pg.Result, allocator: std.mem.Allocator, optionalPrefix: ?[]const u8) !PgMapper(T) {
var column_indexes: [std.meta.fields(T).len]?usize = undefined;
inline for (std.meta.fields(T), 0..) |field, i| {
if (optionalPrefix) |prefix| {
const fullName = try std.fmt.allocPrint(allocator, "{s}" ++ field.name, .{prefix});
defer allocator.free(fullName);
column_indexes[i] = result.columnIndex(fullName);
} else {
column_indexes[i] = result.columnIndex(field.name);
}
}
return .{
.result = result,
.allocator = allocator,
.column_indexes = column_indexes,
};
}
/// PostgreSQL implementation of the query result reader.
pub fn QueryResultReader(comptime TableShape: type, comptime MetadataShape: ?type, comptime inlineRelationships: ?[]const _relationships.Relationship) type {
const InstanceInterface = _result.QueryResultReader(TableShape, MetadataShape, inlineRelationships).Instance;
// Build relationships mappers container type.
const RelationshipsMappersType = comptime typeBuilder: {
if (inlineRelationships) |_inlineRelationships| {
// Make a field for each relationship.
var fields: [_inlineRelationships.len]std.builtin.Type.StructField = undefined;
for (_inlineRelationships, &fields) |relationship, *field| {
// Get relationship field type (TableShape of the related value).
const relationshipFieldType = PgMapper(relationship.TableShape);
field.* = .{
.name = relationship.field ++ [0:0]u8{},
.type = relationshipFieldType,
.default_value = null,
.is_comptime = false,
.alignment = @alignOf(relationshipFieldType),
};
}
// Build type with one field for each relationship.
break :typeBuilder @Type(std.builtin.Type{
.Struct = .{
.layout = std.builtin.Type.ContainerLayout.auto,
.fields = &fields,
.decls = &[0]std.builtin.Type.Declaration{},
.is_tuple = false,
},
});
}
// Build default empty type.
break :typeBuilder @Type(std.builtin.Type{
.Struct = .{
.layout = std.builtin.Type.ContainerLayout.auto,
.fields = &[0]std.builtin.Type.StructField{},
.decls = &[0]std.builtin.Type.Declaration{},
.is_tuple = false,
},
});
};
return struct {
const Self = @This();
/// PostgreSQL implementation of the query result reader instance.
pub const Instance = struct {
/// Main object mapper.
mainMapper: PgMapper(TableShape) = undefined,
metadataMapper: PgMapper(MetadataShape orelse struct {}) = undefined,
relationshipsMappers: RelationshipsMappersType = undefined,
fn next(opaqueSelf: *anyopaque) !?_result.TableWithRelationships(TableShape, MetadataShape, inlineRelationships) {
const self: *Instance = @ptrCast(@alignCast(opaqueSelf));
// Try to get the next row.
var row: pg.Row = try self.mainMapper.result.next() orelse return null;
// Get main table result.
const mainTable = try self.mainMapper.next(&row) orelse return null;
// Initialize the result.
var result: _result.TableWithRelationships(TableShape, MetadataShape, inlineRelationships) = undefined;
// Copy each basic table field.
inline for (std.meta.fields(TableShape)) |field| {
@field(result, field.name) = @field(mainTable, field.name);
}
if (inlineRelationships) |_inlineRelationships| {
// For each relationship, retrieve its value and put it in the result.
inline for (_inlineRelationships) |relationship| {
@field(result, relationship.field) = @field(self.relationshipsMappers, relationship.field).next(&row) catch null;
}
}
if (MetadataShape) |_| {
result._zrm_metadata = (try self.metadataMapper.next(&row)).?;
}
return result; // Return built result.
}
/// Get the generic reader instance instance.
pub fn instance(self: *Instance, allocator: std.mem.Allocator) InstanceInterface {
return .{
.__interface = .{
.instance = self,
.next = next,
},
.allocator = allocator,
};
}
};
instance: Instance = Instance{},
/// The PostgreSQL query result.
result: *pg.Result,
fn initInstance(opaqueSelf: *anyopaque, allocator: std.mem.Allocator) !InstanceInterface {
const self: *Self = @ptrCast(@alignCast(opaqueSelf));
self.instance.mainMapper = try makeMapper(TableShape, self.result, allocator, null);
if (MetadataShape) |MetadataType| {
self.instance.metadataMapper = try makeMapper(MetadataType, self.result, allocator, null);
}
if (inlineRelationships) |_inlineRelationships| {
// Initialize mapper for each relationship.
inline for (_inlineRelationships) |relationship| {
@field(self.instance.relationshipsMappers, relationship.field) =
try makeMapper(relationship.TableShape, self.result, allocator, "relationships." ++ relationship.field ++ ".");
}
}
return self.instance.instance(allocator);
}
/// Get the generic reader instance.
pub fn reader(self: *Self) _result.QueryResultReader(TableShape, MetadataShape, inlineRelationships) {
return .{
._interface = .{
.instance = self,
.init = initInstance,
},
};
}
/// Initialize a PostgreSQL query result reader from the given query result.
pub fn init(result: *pg.Result) Self {
return .{
.result = result,
};
}
};
}

View file

@ -1,354 +1,263 @@
const std = @import("std"); const std = @import("std");
const pg = @import("pg"); const pg = @import("pg");
const zollections = @import("zollections"); const zollections = @import("zollections");
const ZrmError = @import("errors.zig").ZrmError; const errors = @import("errors.zig");
const database = @import("database.zig");
const postgresql = @import("postgresql.zig"); const postgresql = @import("postgresql.zig");
const _sql = @import("sql.zig"); const _sql = @import("sql.zig");
const _conditions = @import("conditions.zig"); const conditions = @import("conditions.zig");
const _relationships = @import("relationships.zig");
const repository = @import("repository.zig"); const repository = @import("repository.zig");
const _comptime = @import("comptime.zig");
const _result = @import("result.zig");
/// Repository query configuration structure. /// Repository query configuration structure.
pub const RepositoryQueryConfiguration = struct { pub const RepositoryQueryConfiguration = struct {
select: ?_sql.RawQuery = null, select: ?_sql.SqlParams = null,
join: ?_sql.RawQuery = null, join: ?_sql.SqlParams = null,
where: ?_sql.RawQuery = null, where: ?_sql.SqlParams = null,
};
/// Compiled relationships structure.
const CompiledRelationships = struct {
inlineRelationships: []_relationships.Relationship,
otherRelationships: []_relationships.Relationship,
inlineSelect: []const u8,
inlineJoins: []const u8,
}; };
/// Repository models query manager. /// Repository models query manager.
/// Manage query string build and its execution. /// Manage query string build and its execution.
pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape), comptime with: ?[]const _relationships.Relationship, comptime MetadataShape: ?type) type { pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape)) type {
const compiledRelationships = comptime compile: { // Pre-compute SQL buffer size.
// Inline relationships list. const selectClause = "SELECT";
var inlineRelationships: []_relationships.Relationship = &[0]_relationships.Relationship{}; const fromClause = "FROM";
// Other relationships list. const whereClause = "WHERE";
var otherRelationships: []_relationships.Relationship = &[0]_relationships.Relationship{}; // SELECT ? FROM {repositoryConfig.table}??;
const fixedSqlSize = selectClause.len + 1 + 0 + 1 + fromClause.len + 1 + repositoryConfig.table.len + 0 + 0 + 1;
if (with) |_with| { const defaultSelectSql = "*";
// If there are relationships to eager load, prepare their query.
// Initialize inline select array.
var inlineSelect: [][]const u8 = &[0][]const u8{};
// Initialize inline joins array.
var inlineJoins: [][]const u8 = &[0][]const u8{};
for (_with) |relationship| {
// For each relationship, determine if it's inline or not.
if (relationship.inlineMapping) {
// Add the current relationship to inline relationships.
inlineRelationships = @ptrCast(@constCast(_comptime.append(inlineRelationships, relationship)));
// Generate selected columns for the relationship.
inlineSelect = @ptrCast(@constCast(_comptime.append(inlineSelect, relationship.select)));
// Generate joined table for the relationship.
inlineJoins = @ptrCast(@constCast(_comptime.append(inlineJoins, relationship.join)));
} else {
// Add the current relationship to other relationships.
otherRelationships = @ptrCast(@constCast(_comptime.append(otherRelationships, relationship)));
}
}
break :compile CompiledRelationships{
.inlineRelationships = inlineRelationships,
.otherRelationships = otherRelationships,
.inlineSelect = if (inlineSelect.len > 0) ", " ++ _comptime.join(", ", inlineSelect) else "",
.inlineJoins = if (inlineJoins.len > 0) " " ++ _comptime.join(" ", inlineJoins) else "",
};
} else {
break :compile CompiledRelationships{
.inlineRelationships = &[0]_relationships.Relationship{},
.otherRelationships = &[0]_relationships.Relationship{},
.inlineSelect = "",
.inlineJoins = "",
};
}
};
// Pre-compute SQL buffer.
const fromClause = " FROM \"" ++ repositoryConfig.table ++ "\"";
const defaultSelectSql = "\"" ++ repositoryConfig.table ++ "\".*" ++ compiledRelationships.inlineSelect;
const defaultJoin = compiledRelationships.inlineJoins;
// Model key type.
const KeyType = repository.ModelKeyType(Model, TableShape, repositoryConfig);
return struct { return struct {
const Self = @This(); const Self = @This();
/// Result mapper type.
pub const ResultMapper = _result.ResultMapper(Model, TableShape, MetadataShape, repositoryConfig, compiledRelationships.inlineRelationships, compiledRelationships.otherRelationships);
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
connector: database.Connector, database: *pg.Pool,
connection: *database.Connection = undefined,
queryConfig: RepositoryQueryConfiguration, queryConfig: RepositoryQueryConfiguration,
query: ?_sql.RawQuery = null,
sql: ?[]const u8 = null, sql: ?[]const u8 = null,
/// Set selected columns. /// Set selected columns.
pub fn select(self: *Self, _select: _sql.RawQuery) void { pub fn select(self: *Self, _select: _sql.SqlParams) void {
self.queryConfig.select = _select; self.queryConfig.select = _select;
} }
/// Set selected columns for SELECT clause. /// Set selected columns for SELECT clause.
pub fn selectColumns(self: *Self, _select: []const []const u8) !void { pub fn selectColumns(self: *Self, _select: []const []const u8) !void {
if (_select.len == 0) { if (_select.len == 0) {
return ZrmError.AtLeastOneSelectionRequired; return errors.AtLeastOneSelectionRequired;
} }
self.select(.{ self.select(.{
// Join selected columns. // Join selected columns.
.sql = std.mem.join(self.arena.allocator(), ", ", _select), .sql = std.mem.join(self.arena.allocator(), ", ", _select),
.params = &[_]_sql.RawQueryParameter{}, // No parameters. .params = &[_]_sql.QueryParameter{}, // No parameters.
}); });
} }
/// Set JOIN clause. /// Set JOIN clause.
pub fn join(self: *Self, _join: _sql.RawQuery) void { pub fn join(self: *Self, _join: _sql.SqlParams) void {
self.queryConfig.join = _join; self.queryConfig.join = _join;
} }
/// Set WHERE conditions. /// Set WHERE conditions.
pub fn where(self: *Self, _where: _sql.RawQuery) void { pub fn where(self: *Self, _where: _sql.SqlParams) void {
self.queryConfig.where = _where; self.queryConfig.where = _where;
} }
/// Create a new condition builder. /// Create a new condition builder.
pub fn newCondition(self: *Self) _conditions.Builder { pub fn newCondition(self: *Self) conditions.Builder {
return _conditions.Builder.init(self.arena.allocator()); return conditions.Builder.init(self.arena.allocator());
} }
/// Set a WHERE value condition. /// Set a WHERE value condition.
pub fn whereValue(self: *Self, comptime ValueType: type, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !void { pub fn whereValue(self: *Self, comptime ValueType: type, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !void {
self.where( self.where(
try _conditions.value(ValueType, self.arena.allocator(), _column, operator, _value) try conditions.value(ValueType, self.arena.allocator(), _column, operator, _value)
); );
} }
/// Set a WHERE column condition. /// Set a WHERE column condition.
pub fn whereColumn(self: *Self, comptime _column: []const u8, comptime operator: []const u8, comptime _valueColumn: []const u8) !void { pub fn whereColumn(self: *Self, comptime _column: []const u8, comptime operator: []const u8, comptime _valueColumn: []const u8) !void {
self.where( self.where(
try _conditions.column(self.arena.allocator(), _column, operator, _valueColumn) try conditions.column(self.arena.allocator(), _column, operator, _valueColumn)
); );
} }
/// Set a WHERE IN condition. /// Set a WHERE IN condition.
pub fn whereIn(self: *Self, comptime ValueType: type, comptime _column: []const u8, _value: []const ValueType) !void { pub fn whereIn(self: *Self, comptime ValueType: type, comptime _column: []const u8, _value: []const ValueType) !void {
self.where( self.where(
try _conditions.in(ValueType, self.arena.allocator(), _column, _value) try conditions.in(ValueType, self.arena.allocator(), _column, _value)
); );
} }
/// Set a WHERE from model key(s).
/// For simple keys: modelKey type must match the type of its corresponding field.
/// modelKey can be an array / slice of keys.
/// For composite keys: modelKey must be a struct with all the keys, matching the type of their corresponding field.
/// modelKey can be an array / slice of these structs.
pub fn whereKey(self: *Self, modelKey: anytype) !void {
if (repositoryConfig.key.len == 1) {
// Find key name and its type.
const keyName = repositoryConfig.key[0];
const qualifiedKeyName = "\"" ++ repositoryConfig.table ++ "\"." ++ keyName;
const keyType = std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, keyName).?].type;
// Accept arrays / slices of keys, and simple keys.
switch (@typeInfo(@TypeOf(modelKey))) {
.Pointer => |ptr| {
switch (ptr.size) {
.One => {
switch (@typeInfo(ptr.child)) {
// Add a whereIn with the array.
.Array => {
if (ptr.child == u8)
// If the child is a string, use it as a simple value.
try self.whereValue(KeyType, qualifiedKeyName, "=", modelKey)
else
// Otherwise, use it as an array.
try self.whereIn(keyType, qualifiedKeyName, modelKey);
},
// Add a simple condition with the pointed value.
else => try self.whereValue(keyType, qualifiedKeyName, "=", modelKey.*),
}
},
// Add a whereIn with the slice.
else => {
if (ptr.child == u8)
// If the child is a string, use it as a simple value.
try self.whereValue(KeyType, qualifiedKeyName, "=", modelKey)
else
// Otherwise, use it as an array.
try self.whereIn(keyType, qualifiedKeyName, modelKey);
},
}
},
// Add a simple condition with the given value.
else => try self.whereValue(keyType, qualifiedKeyName, "=", modelKey),
}
} else {
// Accept arrays / slices of keys, and simple keys.
// Uniformize modelKey parameter to a slice.
const modelKeysList: []const KeyType = switch (@typeInfo(@TypeOf(modelKey))) {
.Pointer => |ptr| switch (ptr.size) {
.One => switch (@typeInfo(ptr.child)) {
// Already an array.
.Array => @as([]const KeyType, modelKey),
// Convert the pointer to an array.
else => &[1]KeyType{@as(KeyType, modelKey.*)},
},
// Already a slice.
else => @as([]const KeyType, modelKey),
},
// Convert the value to an array.
else => &[1]KeyType{@as(KeyType, modelKey)},
};
// Initialize keys conditions list.
const conditions: []_sql.RawQuery = try self.arena.allocator().alloc(_sql.RawQuery, modelKeysList.len);
defer self.arena.allocator().free(conditions);
// For each model key, add its conditions.
for (modelKeysList, conditions) |_modelKey, *condition| {
condition.* = try self.newCondition().@"and"(
&try buildCompositeKeysConditions(TableShape, repositoryConfig.key, self.newCondition(), _modelKey)
);
}
// Set WHERE conditions in the query with all keys conditions.
self.where(try self.newCondition().@"or"(conditions));
}
}
/// Build SQL query. /// Build SQL query.
pub fn buildSql(self: *Self) !void { pub fn buildSql(self: *Self) !void {
// Build the full SQL query from all its parts. // Start parameter counter at 1.
const sqlQuery = _sql.RawQuery{ var currentParameter: usize = 1;
.sql = try std.mem.join(self.arena.allocator(), "", &[_][]const u8{
"SELECT ", if (self.queryConfig.select) |_select| _select.sql else defaultSelectSql, // Compute SELECT size.
fromClause, var selectSize: usize = defaultSelectSql.len;
defaultJoin, if (self.queryConfig.select) |_select| {
if (self.queryConfig.join) |_| " " else "", selectSize = _select.sql.len + _sql.computeRequiredSpaceForParametersNumbers(_select.params.len, currentParameter - 1);
if (self.queryConfig.join) |_join| _join.sql else "", currentParameter += _select.params.len;
if (self.queryConfig.where) |_| " WHERE " else "", }
if (self.queryConfig.where) |_where| _where.sql else "",
";", // Compute JOIN size.
}), var joinSize: usize = 0;
.params = try std.mem.concat(self.arena.allocator(), _sql.RawQueryParameter, &[_][]const _sql.RawQueryParameter{ if (self.queryConfig.join) |_join| {
if (self.queryConfig.select) |_select| _select.params else &[0]_sql.RawQueryParameter{}, joinSize = 1 + _join.sql.len + _sql.computeRequiredSpaceForParametersNumbers(_join.params.len, currentParameter - 1);
if (self.queryConfig.join) |_join| _join.params else &[0]_sql.RawQueryParameter{}, currentParameter += _join.params.len;
if (self.queryConfig.where) |_where| _where.params else &[0]_sql.RawQueryParameter{}, }
})
}; // Compute WHERE size.
var whereSize: usize = 0;
if (self.queryConfig.where) |_where| {
whereSize = 1 + whereClause.len + _where.sql.len + 1 + _sql.computeRequiredSpaceForParametersNumbers(_where.params.len, currentParameter - 1);
currentParameter += _where.params.len;
}
// Allocate SQL buffer from computed size.
const sqlBuf = try self.arena.allocator().alloc(u8, fixedSqlSize
+ (selectSize)
+ (joinSize)
+ (whereSize)
);
// Fill SQL buffer.
// Restart parameter counter at 1.
currentParameter = 1;
// SELECT clause.
@memcpy(sqlBuf[0..selectClause.len+1], selectClause ++ " ");
var sqlBufCursor: usize = selectClause.len+1;
// Copy SELECT clause content and replace parameters, if there are some.
try _sql.copyAndReplaceSqlParameters(&currentParameter,
if (self.queryConfig.select) |_select| _select.params.len else 0,
sqlBuf[sqlBufCursor..sqlBufCursor+selectSize],
if (self.queryConfig.select) |_select| _select.sql else defaultSelectSql,
);
sqlBufCursor += selectSize;
// FROM clause.
sqlBuf[sqlBufCursor] = ' '; sqlBufCursor += 1;
std.mem.copyForwards(u8, sqlBuf[sqlBufCursor..sqlBufCursor+fromClause.len], fromClause); sqlBufCursor += fromClause.len;
sqlBuf[sqlBufCursor] = ' '; sqlBufCursor += 1;
// Table name.
std.mem.copyForwards(u8, sqlBuf[sqlBufCursor..sqlBufCursor+repositoryConfig.table.len], repositoryConfig.table); sqlBufCursor += repositoryConfig.table.len;
// JOIN clause.
if (self.queryConfig.join) |_join| {
sqlBuf[sqlBufCursor] = ' ';
// Copy JOIN clause and replace parameters, if there are some.
try _sql.copyAndReplaceSqlParameters(&currentParameter,
_join.params.len,
sqlBuf[sqlBufCursor+1..sqlBufCursor+joinSize], _join.sql
);
sqlBufCursor += joinSize;
}
// WHERE clause.
if (self.queryConfig.where) |_where| {
@memcpy(sqlBuf[sqlBufCursor..sqlBufCursor+(1 + whereClause.len + 1)], " " ++ whereClause ++ " ");
// Copy WHERE clause content and replace parameters, if there are some.
try _sql.copyAndReplaceSqlParameters(&currentParameter,
_where.params.len,
sqlBuf[sqlBufCursor+(1+whereClause.len+1)..sqlBufCursor+whereSize], _where.sql
);
sqlBufCursor += whereSize;
}
// ";" to end the query.
sqlBuf[sqlBufCursor] = ';'; sqlBufCursor += 1;
// Save built SQL query. // Save built SQL query.
self.query = sqlQuery; self.sql = sqlBuf;
self.sql = try sqlQuery.build(self.arena.allocator());
} }
/// Execute the built query. /// Execute the built query.
fn execQuery(self: *Self) !*pg.Result { fn execQuery(self: *Self) !*pg.Result
// Get the connection to the database. {
self.connection = try self.connector.getConnection(); // Get a connection to the database.
errdefer self.connection.release(); const connection = try self.database.acquire();
errdefer connection.release();
// Initialize a new PostgreSQL statement. // Initialize a new PostgreSQL statement.
var statement = try pg.Stmt.init(self.connection.connection, .{ var statement = try pg.Stmt.init(connection, .{
.column_names = true, .column_names = true,
.release_conn = true,
.allocator = self.arena.allocator(), .allocator = self.arena.allocator(),
}); });
errdefer statement.deinit(); errdefer statement.deinit();
// Prepare SQL query. // Prepare SQL query.
statement.prepare(self.sql.?) statement.prepare(self.sql.?)
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement); catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
// Bind query parameters. // Bind query parameters.
postgresql.bindQueryParameters(&statement, self.query.?.params) if (self.queryConfig.select) |_select|
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement); try postgresql.bindQueryParameters(&statement, _select.params);
if (self.queryConfig.join) |_join|
try postgresql.bindQueryParameters(&statement, _join.params);
if (self.queryConfig.where) |_where|
try postgresql.bindQueryParameters(&statement, _where.params);
// Execute the query and get its result. // Execute the query and get its result.
const result = statement.execute() const result = statement.execute()
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement); catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
// Query executed successfully, return the result. // Query executed successfully, return the result.
return result; return result;
} }
/// Generic queried models retrieval. /// Retrieve queried models.
fn _get(self: *Self, allocator: std.mem.Allocator, comptime withMetadata: bool) !repository.RepositoryResult(if (withMetadata) _result.ModelWithMetadata(Model, MetadataShape) else Model) { pub fn get(self: *Self, allocator: std.mem.Allocator) !repository.RepositoryResult(Model) {
// Build SQL query if it wasn't built. // Build SQL query if it wasn't built.
if (self.sql) |_| {} else { try self.buildSql(); } if (self.sql) |_| {} else { try self.buildSql(); }
// Execute query and get its result. // Execute query and get its result.
var queryResult = try self.execQuery(); var queryResult = try self.execQuery();
defer self.connection.release();
defer queryResult.deinit(); defer queryResult.deinit();
// Map query results. //TODO deduplicate this in postgresql.zig, we could do it if Mapper type was exposed.
var postgresqlReader = postgresql.QueryResultReader(TableShape, MetadataShape, compiledRelationships.inlineRelationships).init(queryResult); //TODO make a generic mapper and do it in repository.zig?
return try ResultMapper.map(withMetadata, allocator, self.connector, postgresqlReader.reader()); // Create an arena for mapper data.
} var mapperArena = std.heap.ArenaAllocator.init(allocator);
// Get result mapper.
const mapper = queryResult.mapper(TableShape, .{ .allocator = mapperArena.allocator() });
/// Retrieve queried models. // Initialize models list.
pub fn get(self: *Self, allocator: std.mem.Allocator) !repository.RepositoryResult(Model) { var models = std.ArrayList(*Model).init(allocator);
return self._get(allocator, false); defer models.deinit();
}
/// Retrieved queries models with metadata. // Get all raw models from the result mapper.
pub fn getWithMetadata(self: *Self, allocator: std.mem.Allocator) !repository.RepositoryResult(_result.ModelWithMetadata(Model, MetadataShape)) { while (try mapper.next()) |rawModel| {
if (MetadataShape) |_| { // Parse each raw model from the mapper.
return self._get(allocator, true); const model = try allocator.create(Model);
} else { model.* = try repositoryConfig.fromSql(rawModel);
unreachable; try models.append(model);
} }
// Return a result with the models.
return repository.RepositoryResult(Model).init(allocator,
zollections.Collection(Model).init(allocator, try models.toOwnedSlice()),
mapperArena,
);
} }
/// Initialize a new repository query. /// Initialize a new repository query.
pub fn init(allocator: std.mem.Allocator, connector: database.Connector, queryConfig: RepositoryQueryConfiguration) Self { pub fn init(allocator: std.mem.Allocator, database: *pg.Pool, queryConfig: RepositoryQueryConfiguration) Self {
return .{ return .{
// Initialize the query arena allocator. // Initialize the query arena allocator.
.arena = std.heap.ArenaAllocator.init(allocator), .arena = std.heap.ArenaAllocator.init(allocator),
.connector = connector, .database = database,
.queryConfig = queryConfig, .queryConfig = queryConfig,
}; };
} }
/// Deinitialize the repository query. /// Deinitialize the repository query.
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
// Free everything allocated for this query.
self.arena.deinit(); self.arena.deinit();
} }
}; };
} }
/// Build conditions for given composite keys, with a model key structure.
pub fn buildCompositeKeysConditions(comptime TableShape: type, comptime keys: []const []const u8, conditionsBuilder: _conditions.Builder, modelKey: anytype) ![keys.len]_sql.RawQuery {
// Conditions list for all keys in the composite key.
var conditions: [keys.len]_sql.RawQuery = undefined;
inline for (keys, &conditions) |keyName, *condition| {
const keyType = std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, keyName).?].type;
if (std.meta.fieldIndex(@TypeOf(modelKey), keyName)) |_| {
// The field exists in the key structure, create its condition.
condition.* = try conditionsBuilder.value(keyType, keyName, "=", @field(modelKey, keyName));
} else {
// The field doesn't exist, compilation error.
@compileError("The key structure must include a field for " ++ keyName);
}
}
// Return conditions for the current model key.
return conditions;
}

View file

@ -1,411 +0,0 @@
const std = @import("std");
const pg = @import("pg");
const _database = @import("database.zig");
const _sql = @import("sql.zig");
const repository = @import("repository.zig");
const _query = @import("query.zig");
/// Configure a "one to many" or "many to many" relationship.
pub const ManyConfiguration = union(enum) {
/// Direct one-to-many relationship using a distant foreign key.
direct: struct {
/// The distant foreign key name pointing to the current model.
foreignKey: []const u8,
/// Current model key name.
/// Use the default key name of the current model.
modelKey: ?[]const u8 = null,
},
/// Used when performing a many-to-many relationship through an association table.
through: struct {
/// Name of the join table.
table: []const u8,
/// The local foreign key name.
/// Use the default key name of the current model.
foreignKey: ?[]const u8 = null,
/// The foreign key name in the join table.
joinForeignKey: []const u8,
/// The model key name in the join table.
joinModelKey: []const u8,
/// Associated model key name.
/// Use the default key name of the associated model.
modelKey: ?[]const u8 = null,
},
};
/// Make a "one to many" or "many to many" relationship.
pub fn many(comptime fromRepo: anytype, comptime toRepo: anytype, comptime config: ManyConfiguration) type {
return typedMany(
fromRepo.ModelType, fromRepo.TableType, fromRepo.config,
toRepo.ModelType, toRepo.TableType, toRepo.config,
config,
);
}
/// Internal implementation of a new "one to many" or "many to many" relationship.
pub fn typedMany(
comptime FromModel: type, comptime FromTable: type,
comptime fromRepositoryConfig: repository.RepositoryConfiguration(FromModel, FromTable),
comptime ToModel: type, comptime ToTable: type,
comptime toRepositoryConfig: repository.RepositoryConfiguration(ToModel, ToTable),
comptime config: ManyConfiguration) type {
return struct {
/// Relationship implementation.
pub fn Implementation(field: []const u8) type {
// Get foreign key from relationship config or repository config.
const foreignKey = switch (config) {
.direct => |direct| direct.foreignKey,
.through => |through| if (through.foreignKey) |_foreignKey| _foreignKey else toRepositoryConfig.key[0],
};
// Get model key from relationship config or repository config.
const modelKey = switch (config) {
.direct => |direct| if (direct.modelKey) |_modelKey| _modelKey else fromRepositoryConfig.key[0],
.through => |through| if (through.modelKey) |_modelKey| _modelKey else fromRepositoryConfig.key[0],
};
_ = modelKey;
const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type;
const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null, struct {
__zrm_relationship_key: FromKeyType,
});
const alias = "relationships." ++ field;
const prefix = alias ++ ".";
return struct {
const Self = @This();
fn genSelect() []const u8 {
return _sql.SelectBuild(ToTable, alias, prefix);
}
fn buildQuery(opaqueModels: []const *anyopaque, allocator: std.mem.Allocator, connector: _database.Connector) !*anyopaque {
const models: []const *FromModel = @ptrCast(@alignCast(opaqueModels));
// Initialize the query to build.
const query: *QueryType = try allocator.create(QueryType);
errdefer allocator.destroy(query);
query.* = QueryType.init(allocator, connector, .{});
errdefer query.deinit();
// Build base SELECT.
const baseSelect = comptime _sql.SelectBuild(ToTable, toRepositoryConfig.table, "");
// Prepare given models IDs.
const modelsIds = try query.arena.allocator().alloc(FromKeyType, models.len);
for (models, modelsIds) |model, *modelId| {
modelId.* = @field(model, fromRepositoryConfig.key[0]);
}
switch (config) {
.direct => {
// Add SELECT.
query.select(.{
.sql = baseSelect ++ ", \"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\" AS \"__zrm_relationship_key\"",
.params = &[0]_sql.RawQueryParameter{},
});
// Build WHERE condition.
try query.whereIn(FromKeyType, "\"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\"", modelsIds);
},
.through => |through| {
// Add SELECT.
query.select(.{
.sql = baseSelect ++ ", \"" ++ prefix ++ "pivot" ++ "\".\"" ++ through.joinModelKey ++ "\" AS \"__zrm_relationship_key\"",
.params = &[0]_sql.RawQueryParameter{},
});
query.join(.{
.sql = "INNER JOIN \"" ++ through.table ++ "\" AS \"" ++ prefix ++ "pivot" ++ "\" " ++
"ON \"" ++ toRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = " ++ "\"" ++ prefix ++ "pivot" ++ "\"." ++ through.joinForeignKey,
.params = &[0]_sql.RawQueryParameter{},
});
// Build WHERE condition.
try query.whereIn(FromKeyType, "\"" ++ prefix ++ "pivot" ++ "\".\"" ++ through.joinModelKey ++ "\"", modelsIds);
},
}
return query; // Return built query.
}
/// Build the "many" generic relationship.
pub fn relationship(_: Self) Relationship {
return .{
._interface = .{
.repositoryConfiguration = &toRepositoryConfig,
.buildQuery = buildQuery,
},
.Model = ToModel,
.TableShape = ToTable,
.field = field,
.alias = alias,
.prefix = prefix,
.QueryType = QueryType,
.inlineMapping = false,
.join = undefined,
.select = genSelect(),
};
}
};
}
};
}
/// Configure a "one to one" relationship.
pub const OneConfiguration = union(enum) {
/// Direct one-to-one relationship using a local foreign key.
direct: struct {
/// The local foreign key name.
foreignKey: []const u8,
/// Associated model key name.
/// Use the default key name of the associated model.
modelKey: ?[]const u8 = null,
},
/// Reverse one-to-one relationship using distant foreign key.
reverse: struct {
/// The distant foreign key name.
/// Use the default key name of the related model.
foreignKey: ?[]const u8 = null,
/// Current model key name.
/// Use the default key name of the current model.
modelKey: ?[]const u8 = null,
},
/// Used when performing a one-to-one relationship through an association table.
through: struct {
/// Name of the join table.
table: []const u8,
/// The local foreign key name.
/// Use the default key name of the current model.
foreignKey: ?[]const u8 = null,
/// The foreign key name in the join table.
joinForeignKey: []const u8,
/// The model key name in the join table.
joinModelKey: []const u8,
/// Associated model key name.
/// Use the default key name of the associated model.
modelKey: ?[]const u8 = null,
},
};
/// Make a "one to one" relationship.
pub fn one(comptime fromRepo: anytype, comptime toRepo: anytype, comptime config: OneConfiguration) type {
return typedOne(
fromRepo.ModelType, fromRepo.TableType, fromRepo.config,
toRepo.ModelType, toRepo.TableType, toRepo.config,
config,
);
}
/// Internal implementation of a new "one to one" relationship.
fn typedOne(
comptime FromModel: type, comptime FromTable: type,
comptime fromRepositoryConfig: repository.RepositoryConfiguration(FromModel, FromTable),
comptime ToModel: type, comptime ToTable: type,
comptime toRepositoryConfig: repository.RepositoryConfiguration(ToModel, ToTable),
comptime config: OneConfiguration) type {
return struct {
pub fn Implementation(field: []const u8) type {
const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type;
const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null, struct {
__zrm_relationship_key: FromKeyType,
});
// Get foreign key from relationship config or repository config.
const foreignKey = switch (config) {
.direct => |direct| direct.foreignKey,
.reverse => |reverse| if (reverse.foreignKey) |_foreignKey| _foreignKey else toRepositoryConfig.key[0],
.through => |through| if (through.foreignKey) |_foreignKey| _foreignKey else fromRepositoryConfig.key[0],
};
// Get model key from relationship config or repository config.
const modelKey = switch (config) {
.direct => |direct| if (direct.modelKey) |_modelKey| _modelKey else toRepositoryConfig.key[0],
.reverse => |reverse| if (reverse.modelKey) |_modelKey| _modelKey else fromRepositoryConfig.key[0],
.through => |through| if (through.modelKey) |_modelKey| _modelKey else toRepositoryConfig.key[0],
};
const alias = "relationships." ++ field;
const prefix = alias ++ ".";
return struct {
const Self = @This();
fn genJoin() []const u8 {
return switch (config) {
.direct => (
"LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++
"\"" ++ fromRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\" = \"" ++ alias ++ "\".\"" ++ modelKey ++ "\""
),
.reverse => (
"LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++
"\"" ++ fromRepositoryConfig.table ++ "\".\"" ++ modelKey ++ "\" = \"" ++ alias ++ "\".\"" ++ foreignKey ++ "\""
),
.through => |through| (
"LEFT JOIN \"" ++ through.table ++ "\" AS \"" ++ alias ++ "_pivot\" ON " ++
"\"" ++ fromRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\" = " ++ "\"" ++ alias ++ "_pivot\".\"" ++ through.joinForeignKey ++ "\"" ++
" LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++
"\"" ++ alias ++ "_pivot\".\"" ++ through.joinModelKey ++ "\" = " ++ "\"" ++ alias ++ "\".\"" ++ modelKey ++ "\""
),
};
}
fn genSelect() []const u8 {
return _sql.SelectBuild(ToTable, alias, prefix);
}
fn buildQuery(opaqueModels: []const *anyopaque, allocator: std.mem.Allocator, connector: _database.Connector) !*anyopaque {
const models: []const *FromModel = @ptrCast(@alignCast(opaqueModels));
// Initialize the query to build.
const query: *QueryType = try allocator.create(QueryType);
errdefer allocator.destroy(query);
query.* = QueryType.init(allocator, connector, .{});
errdefer query.deinit();
// Build base SELECT.
const baseSelect = comptime _sql.SelectBuild(ToTable, toRepositoryConfig.table, "");
// Prepare given models IDs.
const modelsIds = try query.arena.allocator().alloc(FromKeyType, models.len);
for (models, modelsIds) |model, *modelId| {
modelId.* = @field(model, fromRepositoryConfig.key[0]);
}
switch (config) {
.direct => {
// Add SELECT.
query.select(.{
.sql = baseSelect ++ ", \"" ++ fromRepositoryConfig.table ++ "\".\"" ++ fromRepositoryConfig.key[0] ++ "\" AS \"__zrm_relationship_key\"",
.params = &[0]_sql.RawQueryParameter{},
});
query.join((_sql.RawQuery{
.sql = "INNER JOIN \"" ++ fromRepositoryConfig.table ++ "\" AS \"" ++ prefix ++ "related" ++ "\" ON " ++
"\"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = \"" ++ prefix ++ "related" ++ "\"." ++ foreignKey,
.params = &[0]_sql.RawQueryParameter{},
}));
// Build WHERE condition.
try query.whereIn(FromKeyType, "\"" ++ fromRepositoryConfig.table ++ "\".\"" ++ fromRepositoryConfig.key[0] ++ "\"", modelsIds);
},
.reverse => {
// Add SELECT.
query.select(.{
.sql = baseSelect ++ ", \"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\" AS \"__zrm_relationship_key\"",
.params = &[0]_sql.RawQueryParameter{},
});
// Build WHERE condition.
try query.whereIn(FromKeyType, "\"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\"", modelsIds);
},
.through => |through| {
// Add SELECT.
query.select(.{
.sql = baseSelect ++ ", \"" ++ prefix ++ "pivot" ++ "\".\"" ++ through.joinForeignKey ++ "\" AS \"__zrm_relationship_key\"",
.params = &[0]_sql.RawQueryParameter{},
});
query.join(.{
.sql = "INNER JOIN \"" ++ through.table ++ "\" AS \"" ++ prefix ++ "pivot" ++ "\" ON " ++
"\"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = " ++ "\"" ++ prefix ++ "pivot" ++ "\"." ++ through.joinModelKey,
.params = &[0]_sql.RawQueryParameter{},
});
// Build WHERE condition.
try query.whereIn(FromKeyType, "\"" ++ prefix ++ "pivot" ++ "\".\"" ++ through.joinForeignKey ++ "\"", modelsIds);
},
}
// Return built query.
return query;
}
/// Build the "one" generic relationship.
pub fn relationship(_: Self) Relationship {
return .{
._interface = .{
.repositoryConfiguration = &toRepositoryConfig,
.buildQuery = buildQuery,
},
.Model = ToModel,
.TableShape = ToTable,
.field = field,
.alias = alias,
.prefix = prefix,
.QueryType = QueryType,
.inlineMapping = true,
.join = genJoin(),
.select = genSelect(),
};
}
};
}
};
}
/// Generic model relationship interface.
pub const Relationship = struct {
const Self = @This();
_interface: struct {
repositoryConfiguration: *const anyopaque,
buildQuery: *const fn (models: []const *anyopaque, allocator: std.mem.Allocator, connector: _database.Connector) anyerror!*anyopaque,
},
/// Type of the related model.
Model: type,
/// Type of the related model table.
TableShape: type,
/// Field where to put the related model(s).
field: []const u8,
/// Table alias of the relationship.
alias: []const u8,
/// Prefix of fields of the relationship.
prefix: []const u8,
/// Type of a query of the related models.
QueryType: type,
/// Set if relationship mapping is done inline: this means that it's done at the same time the model is mapped,
/// and that the associated data will be retrieved in the main query.
inlineMapping: bool,
/// In case of inline mapping, the JOIN clause to retrieve the associated data.
join: []const u8,
/// The SELECT clause to retrieve the associated data.
select: []const u8,
/// Build the query to retrieve relationship data.
/// Is always used when inline mapping is not possible, but also when loading relationships lazily.
pub fn buildQuery(self: Self, models: []const *anyopaque, allocator: std.mem.Allocator, connector: _database.Connector) !*anyopaque {
return self._interface.buildQuery(models, allocator, connector);
}
/// Get typed repository configuration for the related model.
pub fn repositoryConfiguration(self: Self) repository.RepositoryConfiguration(self.Model, self.TableShape) {
const repoConfig: *const repository.RepositoryConfiguration(self.Model, self.TableShape)
= @ptrCast(@alignCast(self._interface.repositoryConfiguration));
return repoConfig.*;
}
};
/// Structure of an eager loaded relationship.
pub const Eager = struct {
/// The relationship to eager load.
relationship: Relationship,
/// Subrelationships to eager load.
with: []const Eager,
};

View file

@ -1,9 +1,8 @@
const std = @import("std"); const std = @import("std");
const pg = @import("pg");
const zollections = @import("zollections"); const zollections = @import("zollections");
const database = @import("database.zig");
const _sql = @import("sql.zig"); const _sql = @import("sql.zig");
const _conditions = @import("conditions.zig"); const _conditions = @import("conditions.zig");
const _relationships = @import("relationships.zig");
const query = @import("query.zig"); const query = @import("query.zig");
const insert = @import("insert.zig"); const insert = @import("insert.zig");
const update = @import("update.zig"); const update = @import("update.zig");
@ -51,15 +50,12 @@ pub fn ModelKeyType(comptime Model: type, comptime TableShape: type, comptime co
var fieldName: [keyName.len:0]u8 = undefined; var fieldName: [keyName.len:0]u8 = undefined;
@memcpy(fieldName[0..keyName.len], keyName); @memcpy(fieldName[0..keyName.len], keyName);
// Get current field type.
const fieldType = std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, keyName).?].type;
field.* = .{ field.* = .{
.name = &fieldName, .name = &fieldName,
.type = fieldType, .type = std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, keyName).?].type,
.default_value = null, .default_value = null,
.is_comptime = false, .is_comptime = false,
.alignment = @alignOf(fieldType), .alignment = 0,
}; };
} }
@ -74,115 +70,17 @@ pub fn ModelKeyType(comptime Model: type, comptime TableShape: type, comptime co
} }
} }
/// Model relationships definition type.
pub fn RelationshipsDefinitionType(comptime rawDefinition: anytype) type {
const rawDefinitionType = @typeInfo(@TypeOf(rawDefinition));
// Build relationships fields and implementations fields.
var fields: [1 + rawDefinitionType.Struct.fields.len]std.builtin.Type.StructField = undefined;
var implementationsFields: [rawDefinitionType.Struct.fields.len]std.builtin.Type.StructField = undefined;
inline for (rawDefinitionType.Struct.fields, fields[1..], &implementationsFields) |originalField, *field, *implementationField| {
field.* = std.builtin.Type.StructField{
.name = originalField.name,
.type = _relationships.Relationship,
.default_value = null,
.is_comptime = false,
.alignment = @alignOf(type),
};
const ImplementationType = @field(rawDefinition, originalField.name).Implementation(originalField.name);
implementationField.* = std.builtin.Type.StructField{
.name = originalField.name,
.type = ImplementationType,
.default_value = &ImplementationType{},
.is_comptime = false,
.alignment = @alignOf(ImplementationType),
};
}
// Add implementations field.
const ImplementationsType = @Type(.{
.Struct = std.builtin.Type.Struct{
.layout = std.builtin.Type.ContainerLayout.auto,
.fields = &implementationsFields,
.decls = &[_]std.builtin.Type.Declaration{},
.is_tuple = false,
},
});
fields[0] = std.builtin.Type.StructField{
.name = "_implementations",
.type = ImplementationsType,
.default_value = null,
.is_comptime = false,
.alignment = @alignOf(ImplementationsType),
};
// Return built type.
return @Type(.{
.Struct = std.builtin.Type.Struct{
.layout = std.builtin.Type.ContainerLayout.auto,
.fields = &fields,
.decls = &[_]std.builtin.Type.Declaration{},
.is_tuple = false,
},
});
}
/// Repository of structures of a certain type. /// Repository of structures of a certain type.
pub fn Repository(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: RepositoryConfiguration(Model, TableShape)) type { pub fn Repository(comptime Model: type, comptime TableShape: type, comptime config: RepositoryConfiguration(Model, TableShape)) type {
return struct { return struct {
const Self = @This(); const Self = @This();
pub const ModelType = Model; pub const Query: type = query.RepositoryQuery(Model, TableShape, config);
pub const TableType = TableShape;
pub const config = repositoryConfig;
pub const Query: type = query.RepositoryQuery(Model, TableShape, config, null, null);
pub const Insert: type = insert.RepositoryInsert(Model, TableShape, config, config.insertShape); pub const Insert: type = insert.RepositoryInsert(Model, TableShape, config, config.insertShape);
/// Type of one model key. /// Type of one model key.
pub const KeyType = ModelKeyType(Model, TableShape, config); pub const KeyType = ModelKeyType(Model, TableShape, config);
pub const relationships = struct {
/// Make a "one to one" relationship.
pub fn one(comptime toRepo: anytype, comptime oneConfig: _relationships.OneConfiguration) type {
return _relationships.one(Self, toRepo, oneConfig);
}
/// Make a "one to many" or "many to many" relationship.
pub fn many(comptime toRepo: anytype, comptime manyConfig: _relationships.ManyConfiguration) type {
return _relationships.many(Self, toRepo, manyConfig);
}
/// Define a relationships object for a repository.
pub fn define(rawDefinition: anytype) RelationshipsDefinitionType(rawDefinition) {
const rawDefinitionType = @TypeOf(rawDefinition);
// Initialize final relationships definition.
var definition: RelationshipsDefinitionType(rawDefinition) = undefined;
definition._implementations = .{};
// Check that the definition structure only include known fields.
inline for (std.meta.fieldNames(rawDefinitionType)) |fieldName| {
if (!@hasField(Model, fieldName)) {
@compileError("No corresponding field for relationship " ++ fieldName);
}
// Alter definition structure to set the relationship instance.
@field(definition, fieldName) = @field(definition._implementations, fieldName).relationship();
}
// Return altered definition structure.
return definition;
}
};
pub fn QueryWith(comptime with: []const _relationships.Relationship) type {
return query.RepositoryQuery(Model, TableShape, config, with, null);
}
pub fn InsertCustom(comptime InsertShape: type) type { pub fn InsertCustom(comptime InsertShape: type) type {
return insert.RepositoryInsert(Model, TableShape, config, InsertShape); return insert.RepositoryInsert(Model, TableShape, config, InsertShape);
} }
@ -196,12 +94,67 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime repo
/// modelKey can be an array / slice of keys. /// modelKey can be an array / slice of keys.
/// For composite keys: modelKey must be a struct with all the keys, matching the type of their corresponding field. /// For composite keys: modelKey must be a struct with all the keys, matching the type of their corresponding field.
/// modelKey can be an array / slice of these structs. /// modelKey can be an array / slice of these structs.
pub fn find(allocator: std.mem.Allocator, connector: database.Connector, modelKey: anytype) !RepositoryResult(Model) { pub fn find(allocator: std.mem.Allocator, database: *pg.Pool, modelKey: anytype) !RepositoryResult(Model) {
// Initialize a new query. // Initialize a new query.
var modelQuery = Self.Query.init(allocator, connector, .{}); var modelQuery = Self.Query.init(allocator, database, .{});
defer modelQuery.deinit(); defer modelQuery.deinit();
try modelQuery.whereKey(modelKey); if (config.key.len == 1) {
// Find key name and its type.
const keyName = config.key[0];
const keyType = std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, keyName).?].type;
// Accept arrays / slices of keys, and simple keys.
switch (@typeInfo(@TypeOf(modelKey))) {
.Pointer => |ptr| {
switch (ptr.size) {
.One => {
switch (@typeInfo(ptr.child)) {
// Add a whereIn with the array.
.Array => try modelQuery.whereIn(keyType, keyName, modelKey),
// Add a simple condition with the pointed value.
else => try modelQuery.whereValue(keyType, keyName, "=", modelKey.*),
}
},
// Add a whereIn with the slice.
else => try modelQuery.whereIn(keyType, keyName, modelKey),
}
},
// Add a simple condition with the given value.
else => try modelQuery.whereValue(keyType, keyName, "=", modelKey),
}
} else {
// Accept arrays / slices of keys, and simple keys.
// Uniformize modelKey parameter to a slice.
const modelKeysList: []const Self.KeyType = switch (@typeInfo(@TypeOf(modelKey))) {
.Pointer => |ptr| switch (ptr.size) {
.One => switch (@typeInfo(ptr.child)) {
// Already an array.
.Array => @as([]const Self.KeyType, modelKey),
// Convert the pointer to an array.
else => &[1]Self.KeyType{@as(Self.KeyType, modelKey.*)},
},
// Already a slice.
else => @as([]const Self.KeyType, modelKey),
},
// Convert the value to an array.
else => &[1]Self.KeyType{@as(Self.KeyType, modelKey)},
};
// Initialize keys conditions list.
const conditions: []_sql.SqlParams = try allocator.alloc(_sql.SqlParams, modelKeysList.len);
defer allocator.free(conditions);
// For each model key, add its conditions.
for (modelKeysList, conditions) |_modelKey, *condition| {
condition.* = try modelQuery.newCondition().@"and"(
&try buildCompositeKeysConditions(TableShape, config.key, modelQuery.newCondition(), _modelKey)
);
}
// Set WHERE conditions in the query with all keys conditions.
modelQuery.where(try modelQuery.newCondition().@"or"(conditions));
}
// Execute query and return its result. // Execute query and return its result.
return try modelQuery.get(allocator); return try modelQuery.get(allocator);
@ -209,9 +162,9 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime repo
/// Perform creation of the given new model in the repository. /// Perform creation of the given new model in the repository.
/// The model will be altered with the inserted values. /// The model will be altered with the inserted values.
pub fn create(allocator: std.mem.Allocator, connector: database.Connector, newModel: *Model) !RepositoryResult(Model) { pub fn create(allocator: std.mem.Allocator, database: *pg.Pool, newModel: *Model) !RepositoryResult(Model) {
// Initialize a new insert query for the given model. // Initialize a new insert query for the given model.
var insertQuery = Self.Insert.init(allocator, connector); var insertQuery = Self.Insert.init(allocator, database);
defer insertQuery.deinit(); defer insertQuery.deinit();
try insertQuery.values(newModel); try insertQuery.values(newModel);
insertQuery.returningAll(); insertQuery.returningAll();
@ -229,18 +182,18 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime repo
} }
/// Perform save of the given existing model in the repository. /// Perform save of the given existing model in the repository.
pub fn save(allocator: std.mem.Allocator, connector: database.Connector, existingModel: *Model) !RepositoryResult(Model) { pub fn save(allocator: std.mem.Allocator, database: *pg.Pool, existingModel: *Model) !RepositoryResult(Model) {
// Convert the model to its SQL form. // Convert the model to its SQL form.
const modelSql = try config.toSql(existingModel.*); const modelSql = try config.toSql(existingModel.*);
// Initialize a new update query for the given model. // Initialize a new update query for the given model.
var updateQuery = Self.Update(TableShape).init(allocator, connector); var updateQuery = Self.Update(TableShape).init(allocator, database);
defer updateQuery.deinit(); defer updateQuery.deinit();
try updateQuery.set(modelSql); try updateQuery.set(modelSql);
updateQuery.returningAll(); updateQuery.returningAll();
// Initialize conditions array. // Initialize conditions array.
var conditions: [config.key.len]_sql.RawQuery = undefined; var conditions: [config.key.len]_sql.SqlParams = undefined;
inline for (config.key, &conditions) |keyName, *condition| { inline for (config.key, &conditions) |keyName, *condition| {
// Add a where condition for each key. // Add a where condition for each key.
condition.* = try updateQuery.newCondition().value(@TypeOf(@field(modelSql, keyName)), keyName, "=", @field(modelSql, keyName)); condition.* = try updateQuery.newCondition().value(@TypeOf(@field(modelSql, keyName)), keyName, "=", @field(modelSql, keyName));
@ -301,3 +254,24 @@ pub fn RepositoryResult(comptime Model: type) type {
} }
}; };
} }
/// Build conditions for given composite keys, with a model key structure.
pub fn buildCompositeKeysConditions(comptime TableShape: type, comptime keys: []const []const u8, conditionsBuilder: _conditions.Builder, modelKey: anytype) ![keys.len]_sql.SqlParams {
// Conditions list for all keys in the composite key.
var conditions: [keys.len]_sql.SqlParams = undefined;
inline for (keys, &conditions) |keyName, *condition| {
const keyType = std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, keyName).?].type;
if (std.meta.fieldIndex(@TypeOf(modelKey), keyName)) |_| {
// The field exists in the key structure, create its condition.
condition.* = try conditionsBuilder.value(keyType, keyName, "=", @field(modelKey, keyName));
} else {
// The field doesn't exist, compilation error.
@compileError("The key structure must include a field for " ++ keyName);
}
}
// Return conditions for the current model key.
return conditions;
}

View file

@ -1,260 +0,0 @@
const std = @import("std");
const zollections = @import("zollections");
const _database = @import("database.zig");
const _repository = @import("repository.zig");
const _relationships = @import("relationships.zig");
/// Structure of a model with its metadata.
pub fn ModelWithMetadata(comptime Model: type, comptime MetadataShape: ?type) type {
if (MetadataShape) |MetadataType| {
return struct {
model: Model,
metadata: MetadataType,
};
} else {
return Model;
}
}
/// Type of a retrieved table data, with its retrieved relationships.
pub fn TableWithRelationships(comptime TableShape: type, comptime MetadataShape: ?type, comptime optionalRelationships: ?[]const _relationships.Relationship) type {
if (optionalRelationships) |relationships| {
const tableType = @typeInfo(TableShape);
// Build fields list: copy the existing table type fields and add those for relationships.
var fields: [tableType.Struct.fields.len + relationships.len + (if (MetadataShape) |_| 1 else 0)]std.builtin.Type.StructField = undefined;
// Copy base table fields.
@memcpy(fields[0..tableType.Struct.fields.len], tableType.Struct.fields);
// For each relationship, create a new struct field in the table shape.
for (relationships, fields[tableType.Struct.fields.len..(tableType.Struct.fields.len+relationships.len)]) |relationship, *field| {
// Get relationship field type (optional TableShape of the related value).
const relationshipFieldType = @Type(std.builtin.Type{
.Optional = .{
.child = relationship.TableShape
},
});
// Create the new field from relationship data.
field.* = std.builtin.Type.StructField{
.name = relationship.field ++ [0:0]u8{},
.type = relationshipFieldType,
.default_value = null,
.is_comptime = false,
.alignment = @alignOf(relationshipFieldType),
};
}
if (MetadataShape) |MetadataType| {
// Add metadata field.
fields[tableType.Struct.fields.len + relationships.len] = std.builtin.Type.StructField{
.name = "_zrm_metadata",
.type = MetadataType,
.default_value = null,
.is_comptime = false,
.alignment = @alignOf(MetadataType),
};
}
// Build the new type.
return @Type(std.builtin.Type{
.Struct = .{
.layout = tableType.Struct.layout,
.fields = &fields,
.decls = &[0]std.builtin.Type.Declaration{},
.is_tuple = tableType.Struct.is_tuple,
.backing_integer = tableType.Struct.backing_integer,
},
});
} else {
return TableShape;
}
}
/// Convert a value of the fully retrieved type to the TableShape type.
pub fn toTableShape(comptime TableShape: type, comptime MetadataShape: ?type, comptime optionalRelationships: ?[]const _relationships.Relationship, value: TableWithRelationships(TableShape, MetadataShape, optionalRelationships)) TableShape {
if (optionalRelationships) |_| {
// Make a structure of TableShape type.
var tableValue: TableShape = undefined;
// Copy all fields of the table shape in the new structure.
inline for (std.meta.fields(TableShape)) |field| {
@field(tableValue, field.name) = @field(value, field.name);
}
// Return the simplified structure.
return tableValue;
} else {
// No relationships, it should already be of type TableShape.
return value;
}
}
/// Generic interface of a query result reader.
pub fn QueryResultReader(comptime TableShape: type, comptime MetadataShape: ?type, comptime inlineRelationships: ?[]const _relationships.Relationship) type {
return struct {
const Self = @This();
/// Generic interface of a query result reader instance.
pub const Instance = struct {
__interface: struct {
instance: *anyopaque,
next: *const fn (self: *anyopaque) anyerror!?TableWithRelationships(TableShape, MetadataShape, inlineRelationships),
},
allocator: std.mem.Allocator,
pub fn next(self: Instance) !?TableWithRelationships(TableShape, MetadataShape, inlineRelationships) {
return self.__interface.next(self.__interface.instance);
}
};
_interface: struct {
instance: *anyopaque,
init: *const fn (self: *anyopaque, allocator: std.mem.Allocator) anyerror!Instance,
},
/// Initialize a reader instance.
pub fn init(self: Self, allocator: std.mem.Allocator) !Instance {
return self._interface.init(self._interface.instance, allocator);
}
};
}
/// Map query result to repository model structures, and load the given relationships.
pub fn ResultMapper(comptime Model: type, comptime TableShape: type, comptime MetadataShape: ?type, comptime repositoryConfig: _repository.RepositoryConfiguration(Model, TableShape), comptime inlineRelationships: ?[]const _relationships.Relationship, comptime relationships: ?[]const _relationships.Relationship) type {
return struct {
/// Map the query result to a repository result, with all the required relationships.
pub fn map(comptime withMetadata: bool, allocator: std.mem.Allocator, connector: _database.Connector, queryReader: QueryResultReader(TableShape, MetadataShape, inlineRelationships)) !_repository.RepositoryResult(if (withMetadata) ModelWithMetadata(Model, MetadataShape) else Model) {
// Get result type depending on metadata
const ResultType = if (withMetadata) ModelWithMetadata(Model, MetadataShape) else Model;
// Create an arena for mapper data.
var mapperArena = std.heap.ArenaAllocator.init(allocator);
errdefer mapperArena.deinit();
// Initialize query result reader.
const reader = try queryReader.init(mapperArena.allocator());
// Initialize models list.
var models = std.ArrayList(*ResultType).init(allocator);
defer models.deinit();
// Get all raw models from the result reader.
while (try reader.next()) |rawModel| {
// Parse each raw model from the reader.
const model = try allocator.create(ResultType);
(if (withMetadata) model.model else model.*) = try repositoryConfig.fromSql(toTableShape(TableShape, MetadataShape, inlineRelationships, rawModel));
// Map inline relationships.
if (inlineRelationships) |_inlineRelationships| {
// If there are loaded inline relationships, map them to the result.
inline for (_inlineRelationships) |relationship| {
// Read the inline related value.
const relatedValue = (
if (@field(rawModel, relationship.field)) |relationshipVal|
try relationship.repositoryConfiguration().fromSql(relationshipVal)
else null
);
if (pointedType(@TypeOf(@field(model.*, relationship.field)))) |childType| {
if (relatedValue) |val| {
// Allocate pointer value.
@field(model.*, relationship.field) = try mapperArena.allocator().create(childType);
// Set pointer value.
@field(model.*, relationship.field).?.* = val;
} else {
// Set NULL value.
@field(model.*, relationship.field) = null;
}
} else {
// Set simple value.
@field(model.*, relationship.field) = relatedValue;
}
}
}
if (withMetadata) {
// Set model metadata.
model.metadata = rawModel._zrm_metadata;
}
try models.append(model);
}
errdefer {
// Destroy models when something bad happen.
for (models.items) |model| {
allocator.destroy(model);
}
}
if (relationships) |relationshipsToLoad| {
inline for (relationshipsToLoad) |relationship| {
// Build query for the relationship to get.
const query: *relationship.QueryType = @ptrCast(@alignCast(
try relationship.buildQuery(@ptrCast(models.items), allocator, connector)
));
defer {
query.deinit();
allocator.destroy(query);
}
// Get related models.
const relatedModels = try query.getWithMetadata(mapperArena.allocator());
// Create a map with related models.
const RelatedModelsListType = std.ArrayList(@TypeOf(relatedModels.models[0].model));
const RelatedModelsMapType = std.AutoHashMap(std.meta.FieldType(@TypeOf(relatedModels.models[0].metadata), .__zrm_relationship_key), RelatedModelsListType);
var relatedModelsMap = RelatedModelsMapType.init(allocator);
defer relatedModelsMap.deinit();
// Fill the map of related models, indexing them by the relationship key.
for (relatedModels.models) |relatedModel| {
// For each related model, put it in the map at the relationship key.
var modelsList = try relatedModelsMap.getOrPut(relatedModel.metadata.__zrm_relationship_key);
if (!modelsList.found_existing) {
// Initialize the related models list.
modelsList.value_ptr.* = RelatedModelsListType.init(mapperArena.allocator());
}
// Add the current related model to the list.
try modelsList.value_ptr.append(relatedModel.model);
}
// For each model, at the grouped related models if there are some.
for (models.items) |model| {
@field(model, relationship.field) = (
if (relatedModelsMap.getPtr(@field(model, repositoryConfig.key[0]))) |relatedModelsList|
// There are related models, set them.
try relatedModelsList.toOwnedSlice()
else
// No related models, set an empty array.
&[0](@TypeOf(relatedModels.models[0].model)){}
);
}
}
}
// Return a result with the models.
return _repository.RepositoryResult(ResultType).init(allocator,
zollections.Collection(ResultType).init(allocator, try models.toOwnedSlice()),
mapperArena,
);
}
};
}
/// Get pointed type of the given type.
/// Return NULL if the type is not a pointer.
fn pointedType(@"type": type) ?type {
return switch (@typeInfo(@"type")) {
.Pointer => |ptr| ptr.child,
.Optional => |opt| switch (@typeInfo(opt.child)) {
.Pointer => |ptr| ptr.child,
else => null,
},
else => null,
};
}

View file

@ -1,5 +1,4 @@
const global = @import("global.zig"); const global = @import("global.zig");
const session = @import("session.zig");
const repository = @import("repository.zig"); const repository = @import("repository.zig");
const insert = @import("insert.zig"); const insert = @import("insert.zig");
const _sql = @import("sql.zig"); const _sql = @import("sql.zig");
@ -12,16 +11,9 @@ pub const RepositoryResult = repository.RepositoryResult;
pub const Insertable = insert.Insertable; pub const Insertable = insert.Insertable;
pub const relationships = @import("relationships.zig"); pub const QueryParameter = _sql.QueryParameter;
pub const SqlParams = _sql.SqlParams;
pub const RawQueryParameter = _sql.RawQueryParameter;
pub const RawQuery = _sql.RawQuery;
pub const database = @import("database.zig");
pub const Session = session.Session;
pub const conditions = @import("conditions.zig"); pub const conditions = @import("conditions.zig");
pub const errors = @import("errors.zig"); pub const errors = @import("errors.zig");
pub const helpers = @import("helpers.zig");

View file

@ -1,139 +0,0 @@
const std = @import("std");
const pg = @import("pg");
const postgresql = @import("postgresql.zig");
const database = @import("database.zig");
/// Session for multiple repository operations.
pub const Session = struct {
const Self = @This();
_database: *pg.Pool,
/// The active connection for the session.
connection: *pg.Conn,
/// The count of active transactions for the session.
activeTransactions: usize = 0,
/// Execute a comptime-known SQL command for the current session.
fn exec(self: Self, comptime sql: []const u8) !void {
_ = self.connection.exec(sql, .{}) catch |err| {
return postgresql.handleRawPostgresqlError(err, self.connection);
};
}
/// Begin a new transaction.
pub fn beginTransaction(self: Self) !void {
try self.exec("BEGIN;");
}
/// Rollback the current transaction.
pub fn rollbackTransaction(self: Self) !void {
try self.exec("ROLLBACK;");
}
/// Rollback all active transactions.
pub fn rollbackAll(self: Self) !void {
for (0..self.activeTransactions) |_| {
self.rollbackTransaction();
}
}
/// Commit the current transaction.
pub fn commitTransaction(self: Self) !void {
try self.exec("COMMIT;");
}
/// Commit all active transactions.
pub fn commitAll(self: Self) !void {
for (0..self.activeTransactions) |_| {
self.commitTransaction();
}
}
/// Create a new savepoint with the given name.
pub fn savepoint(self: Self, comptime _savepoint: []const u8) !void {
try self.exec("SAVEPOINT " ++ _savepoint ++ ";");
}
/// Rollback to the savepoint with the given name.
pub fn rollbackTo(self: Self, comptime _savepoint: []const u8) !void {
try self.exec("ROLLBACK TO " ++ _savepoint ++ ";");
}
/// Initialize a new session.
pub fn init(_database: *pg.Pool) !Session {
return .{
._database = _database,
.connection = try _database.acquire(),
};
}
/// Deinitialize the session.
pub fn deinit(self: *Self) void {
self.connection.release();
}
/// Get a database connector instance for the current session.
pub fn connector(self: *Self) database.Connector {
return database.Connector{
._interface = .{
.instance = self,
.getConnection = getConnection,
},
};
}
// Connector implementation.
/// Get the current connection.
fn getConnection(opaqueSelf: *anyopaque) !*database.Connection {
const self: *Self = @ptrCast(@alignCast(opaqueSelf));
// Initialize a new connection.
const sessionConnection = try self._database._allocator.create(SessionConnection);
sessionConnection.* = .{
.session = self,
};
return try sessionConnection.connection();
}
};
fn noRelease(_: *anyopaque) void {}
/// A session connection.
const SessionConnection = struct {
const Self = @This();
/// Session of the connection.
session: *Session,
/// Connection instance, to only keep one at a time.
_connection: ?database.Connection = null,
/// Get a database connection.
pub fn connection(self: *Self) !*database.Connection {
if (self._connection == null) {
// A new connection needs to be initialized.
self._connection = .{
.connection = self.session.connection,
._interface = .{
.instance = self,
.release = releaseConnection,
},
};
}
return &(self._connection.?);
}
// Implementation.
/// Free the current connection (doesn't actually release the connection, as it is required to stay the same all along the session).
fn releaseConnection(self: *database.Connection) void {
// Free allocated connection.
const sessionConnection: *SessionConnection = @ptrCast(@alignCast(self._interface.instance));
sessionConnection.session._database._allocator.destroy(sessionConnection);
}
};

View file

@ -1,68 +1,10 @@
const std = @import("std"); const std = @import("std");
const ZrmError = @import("errors.zig").ZrmError; const errors = @import("errors.zig");
/// A structure with SQL and its parameters. /// A structure with SQL and its parameters.
pub const RawQuery = struct { pub const SqlParams = struct {
const Self = @This();
sql: []const u8, sql: []const u8,
params: []const RawQueryParameter, params: []const QueryParameter,
/// Build an SQL query with all the given query parts, separated by a space.
pub fn fromConcat(allocator: std.mem.Allocator, queries: []const RawQuery) !Self {
// Allocate an array with all SQL queries.
const queriesSql = try allocator.alloc([]const u8, queries.len);
defer allocator.free(queriesSql);
// Allocate an array with all parameters arrays.
const queriesParams = try allocator.alloc([]const RawQueryParameter, queries.len);
defer allocator.free(queriesSql);
// Fill SQL queries and parameters arrays.
for (queries, queriesSql, queriesParams) |_query, *_querySql, *_queryParam| {
_querySql.* = _query.sql;
_queryParam.* = _query.params;
}
// Build final query with its parameters.
return Self{
.sql = try std.mem.join(allocator, " ", queriesSql),
.params = try std.mem.concat(allocator, RawQueryParameter, queriesParams),
};
}
/// Build a full SQL query with numbered parameters.
pub fn build(self: Self, allocator: std.mem.Allocator) ![]u8 {
if (self.params.len <= 0) {
// No parameters, just copy SQL.
return allocator.dupe(u8, self.sql);
} else {
// Copy SQL and replace '?' by numbered parameters.
const sqlSize = self.sql.len + computeRequiredSpaceForNumbers(self.params.len);
var sqlBuf = try std.ArrayList(u8).initCapacity(allocator, sqlSize);
defer sqlBuf.deinit();
// Parameter counter.
var currentParameter: usize = 1;
for (self.sql) |char| {
// Copy each character but '?', replaced by the current parameter string.
if (char == '?') {
// Copy the parameter string in place of '?'.
try sqlBuf.writer().print("${d}", .{currentParameter});
// Increment parameter count.
currentParameter += 1;
} else {
// Simply pass the current character.
try sqlBuf.append(char);
}
}
// Return the built SQL query.
return sqlBuf.toOwnedSlice();
}
}
}; };
/// Generate parameters SQL in the form of "?,?,?,?" /// Generate parameters SQL in the form of "?,?,?,?"
@ -79,166 +21,6 @@ pub fn generateParametersSql(allocator: std.mem.Allocator, parametersCount: u64)
return sql; return sql;
} }
/// Compute required string size of numbers for the given parameters count.
pub fn computeRequiredSpaceForNumbers(parametersCount: usize) usize {
var numbersSize: usize = 0; // Initialize the required size.
var remaining = parametersCount; // Initialize the remaining parameters to count.
var currentSliceSize: usize = 9; // Initialize the first slice size of numbers.
var i: usize = 1; // Initialize the current slice count.
while (remaining > 0) {
// Compute the count of numbers in the current slice.
const numbersCount = @min(remaining, currentSliceSize);
// Add the required string size of all numbers in this slice.
numbersSize += i * numbersCount;
// Subtract the counted numbers in this current slice.
remaining -= numbersCount;
// Move to the next slice.
i += 1;
currentSliceSize *= 10;
}
// Return the computed numbers size.
return numbersSize;
}
/// Compute required string size for the given parameter number.
pub fn computeRequiredSpaceForParameter(parameterNumber: usize) !usize {
var i: usize = 1;
while (parameterNumber >= try std.math.powi(usize, 10, i)) {
i += 1;
}
return i;
}
/// A query parameter.
pub const RawQueryParameter = union(enum) {
string: []const u8,
integer: i64,
number: f64,
bool: bool,
null: void,
/// Convert any value to a query parameter.
pub fn fromValue(value: anytype) ZrmError!RawQueryParameter {
// Get given value type.
const valueType = @typeInfo(@TypeOf(value));
return switch (valueType) {
.Int, .ComptimeInt => return .{ .integer = @intCast(value), },
.Float, .ComptimeFloat => return .{ .number = @floatCast(value), },
.Bool => return .{ .bool = value, },
.Null => return .{ .null = true, },
.Pointer => |pointer| {
if (pointer.size == .One) {
// Get pointed value.
return RawQueryParameter.fromValue(value.*);
} else {
// Can only take an array of u8 (= string).
if (pointer.child == u8) {
return .{ .string = value };
} else {
return ZrmError.UnsupportedTableType;
}
}
},
.Enum, .EnumLiteral => {
return .{ .string = @tagName(value) };
},
.Optional => {
if (value) |val| {
// The optional value is defined, use it as a query parameter.
return RawQueryParameter.fromValue(val);
} else {
// If an optional value is not defined, set it to NULL.
return .{ .null = true };
}
},
else => return ZrmError.UnsupportedTableType
};
}
};
/// SELECT query part builder for a given table.
pub fn SelectBuilder(comptime TableShape: type) type {
// Get fields count in the table shape.
const columnsCount = @typeInfo(TableShape).Struct.fields.len;
// Sum of lengths of all selected columns formats.
var _selectColumnsLength = 0;
const selectColumns = comptime select: {
// Initialize the select columns array.
var _select: [columnsCount][]const u8 = undefined;
// For each field, generate a format string.
for (@typeInfo(TableShape).Struct.fields, &_select) |field, *columnSelect| {
// Select the current field column.
columnSelect.* = "\"{s}\".\"" ++ field.name ++ "\" AS \"{s}" ++ field.name ++ "\"";
_selectColumnsLength = _selectColumnsLength + columnSelect.len;
}
break :select _select;
};
// Export computed select columns length.
const selectColumnsLength = _selectColumnsLength;
return struct {
/// Build a SELECT query part for a given table, renaming columns with the given prefix.
pub fn build(allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) ![]const u8 {
// Initialize full select string with precomputed size.
var fullSelect = try std.ArrayList(u8).initCapacity(allocator,
selectColumnsLength // static SQL size.
+ columnsCount*(table.len - 2 + prefix.len - 2) // replacing %s and %s by table and prefix.
+ (columnsCount - 1) * 2 // ", "
);
defer fullSelect.deinit();
var first = true;
inline for (selectColumns) |columnSelect| {
// Add ", " between all selected columns.
if (first) {
first = false;
} else {
try fullSelect.appendSlice(", ");
}
try fullSelect.writer().print(columnSelect, .{table, prefix});
}
return fullSelect.toOwnedSlice(); // Return built full select.
}
};
}
/// Build a SELECT query part for a given table, renaming columns with the given prefix, at comptime.
pub fn SelectBuild(comptime TableShape: type, comptime table: []const u8, comptime prefix: []const u8) []const u8 {
// Initialize the selected columns string.
comptime var columnsSelect: []const u8 = "";
comptime var first = true;
// For each field, generate a format string.
inline for (@typeInfo(TableShape).Struct.fields) |field| {
// Add ", " between all selected columns (just not the first one).
if (first) {
first = false;
} else {
columnsSelect = @ptrCast(columnsSelect ++ ", ");
}
// Select the current field column.
columnsSelect = @ptrCast(columnsSelect ++ "\"" ++ table ++ "\".\"" ++ field.name ++ "\" AS \"" ++ prefix ++ field.name ++ "\"");
}
// Return built columns selection.
return columnsSelect;
}
/// Compute required string size of numbers for the given parameters count, with taking in account the already used parameters numbers. /// Compute required string size of numbers for the given parameters count, with taking in account the already used parameters numbers.
pub fn computeRequiredSpaceForParametersNumbers(parametersCount: usize, alreadyUsedParameters: usize) usize { pub fn computeRequiredSpaceForParametersNumbers(parametersCount: usize, alreadyUsedParameters: usize) usize {
var remainingUsedParameters = alreadyUsedParameters; // Initialize the count of used parameters to mark as taken. var remainingUsedParameters = alreadyUsedParameters; // Initialize the count of used parameters to mark as taken.
@ -273,25 +55,149 @@ pub fn computeRequiredSpaceForParametersNumbers(parametersCount: usize, alreadyU
return numbersSize; return numbersSize;
} }
/// Copy the given source query and replace '?' parameters by numbered parameters. /// Compute required string size of numbers for the given parameters count.
pub fn copyAndReplaceSqlParameters(currentParameter: *usize, parametersCount: usize, writer: std.ArrayList(u8).Writer, source: []const u8) !void { pub fn computeRequiredSpaceForNumbers(parametersCount: usize) usize {
var numbersSize: usize = 0; // Initialize the required size.
var remaining = parametersCount; // Initialize the remaining parameters to count.
var currentSliceSize: usize = 9; // Initialize the first slice size of numbers.
var i: usize = 1; // Initialize the current slice count.
while (remaining > 0) {
// Compute the count of numbers in the current slice.
const numbersCount = @min(remaining, currentSliceSize);
// Add the required string size of all numbers in this slice.
numbersSize += i * numbersCount;
// Subtract the counted numbers in this current slice.
remaining -= numbersCount;
// Move to the next slice.
i += 1;
currentSliceSize *= 10;
}
// Return the computed numbers size.
return numbersSize;
}
/// Compute required string size for the given parameter number.
pub fn computeRequiredSpaceForParameter(parameterNumber: usize) !usize {
var i: usize = 1;
while (parameterNumber >= try std.math.powi(usize, 10, i)) {
i += 1;
}
return i;
}
pub fn copyAndReplaceSqlParameters(currentParameter: *usize, parametersCount: usize, dest: []u8, source: []const u8) !void {
// If there are no parameters, just copy source SQL. // If there are no parameters, just copy source SQL.
if (parametersCount <= 0) { if (parametersCount <= 0) {
try writer.writeAll(source); std.mem.copyForwards(u8, dest, source);
return;
} }
// Current dest cursor.
var destCursor: usize = 0;
for (source) |char| { for (source) |char| {
// Copy each character but '?', replaced by the current parameter string. // Copy each character but '?', replaced by the current parameter string.
if (char == '?') { if (char == '?') {
// Create the parameter string.
const paramSize = 1 + try computeRequiredSpaceForParameter(currentParameter.*);
// Copy the parameter string in place of '?'. // Copy the parameter string in place of '?'.
try writer.print("${d}", .{currentParameter.*}); _ = try std.fmt.bufPrint(dest[destCursor..destCursor+paramSize], "${d}", .{currentParameter.*});
// Add parameter string length to the current query cursor.
destCursor += paramSize;
// Increment parameter count. // Increment parameter count.
currentParameter.* += 1; currentParameter.* += 1;
} else { } else {
// Simply write the current character. // Simply pass the current character.
try writer.writeByte(char); dest[destCursor] = char;
destCursor += 1;
} }
} }
} }
pub fn numberSqlParameters(sql: []const u8, comptime parametersCount: usize) [sql.len + computeRequiredSpaceForNumbers(parametersCount)]u8 {
// If there are no parameters, just return built SQL.
if (parametersCount <= 0) {
return @as([sql.len]u8, sql[0..sql.len].*);
}
// New query buffer.
var query: [sql.len + computeRequiredSpaceForNumbers(parametersCount)]u8 = undefined;
// Current query cursor.
var queryCursor: usize = 0;
// Current parameter count.
var currentParameter: usize = 1;
for (sql) |char| {
// Copy each character but '?', replaced by the current parameter string.
if (char == '?') {
var buffer: [computeRequiredSpaceForParameter(currentParameter)]u8 = undefined;
// Create the parameter string.
const paramStr = try std.fmt.bufPrint(&buffer, "${d}", .{currentParameter});
// Copy the parameter string in place of '?'.
@memcpy(query[queryCursor..(queryCursor + paramStr.len)], paramStr);
// Add parameter string length to the current query cursor.
queryCursor += paramStr.len;
// Increment parameter count.
currentParameter += 1;
} else {
// Simply pass the current character.
query[queryCursor] = char;
queryCursor += 1;
}
}
// Return built query.
return query;
}
/// A query parameter.
pub const QueryParameter = union(enum) {
string: []const u8,
integer: i64,
number: f64,
bool: bool,
null: void,
/// Convert any value to a query parameter.
pub fn fromValue(value: anytype) errors.ZrmError!QueryParameter {
// Get given value type.
const valueType = @typeInfo(@TypeOf(value));
return switch (valueType) {
.Int, .ComptimeInt => return .{ .integer = @intCast(value), },
.Float, .ComptimeFloat => return .{ .number = @floatCast(value), },
.Bool => return .{ .bool = value, },
.Null => return .{ .null = true, },
.Pointer => |pointer| {
if (pointer.size == .One) {
// Get pointed value.
return QueryParameter.fromValue(value.*);
} else {
// Can only take an array of u8 (= string).
if (pointer.child == u8) {
return .{ .string = value };
} else {
return errors.ZrmError.UnsupportedTableType;
}
}
},
.Enum, .EnumLiteral => {
return .{ .string = @tagName(value) };
},
.Optional => {
if (value) |val| {
// The optional value is defined, use it as a query parameter.
return QueryParameter.fromValue(val);
} else {
// If an optional value is not defined, set it to NULL.
return .{ .null = true };
}
},
else => return errors.ZrmError.UnsupportedTableType
};
}
};

View file

@ -1,20 +1,18 @@
const std = @import("std"); const std = @import("std");
const pg = @import("pg"); const pg = @import("pg");
const zollections = @import("zollections"); const zollections = @import("zollections");
const ZrmError = @import("errors.zig").ZrmError; const errors = @import("errors.zig");
const database = @import("database.zig");
const postgresql = @import("postgresql.zig"); const postgresql = @import("postgresql.zig");
const _sql = @import("sql.zig"); const _sql = @import("sql.zig");
const conditions = @import("conditions.zig"); const conditions = @import("conditions.zig");
const repository = @import("repository.zig"); const repository = @import("repository.zig");
const _result = @import("result.zig");
/// Repository update query configuration structure. /// Repository update query configuration structure.
pub fn RepositoryUpdateConfiguration(comptime UpdateShape: type) type { pub fn RepositoryUpdateConfiguration(comptime UpdateShape: type) type {
return struct { return struct {
value: ?UpdateShape = null, value: ?UpdateShape = null,
where: ?_sql.RawQuery = null, where: ?_sql.SqlParams = null,
returning: ?_sql.RawQuery = null, returning: ?_sql.SqlParams = null,
}; };
} }
@ -58,12 +56,8 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
const Configuration = RepositoryUpdateConfiguration(UpdateShape); const Configuration = RepositoryUpdateConfiguration(UpdateShape);
/// Result mapper type.
pub const ResultMapper = _result.ResultMapper(Model, TableShape, null, repositoryConfig, null, null);
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
connector: database.Connector, database: *pg.Pool,
connection: *database.Connection = undefined,
updateConfig: Configuration, updateConfig: Configuration,
sql: ?[]const u8 = null, sql: ?[]const u8 = null,
@ -117,7 +111,7 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
} }
/// Set WHERE conditions. /// Set WHERE conditions.
pub fn where(self: *Self, _where: _sql.RawQuery) void { pub fn where(self: *Self, _where: _sql.SqlParams) void {
self.updateConfig.where = _where; self.updateConfig.where = _where;
} }
@ -148,20 +142,20 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
} }
/// Set selected columns for RETURNING clause. /// Set selected columns for RETURNING clause.
pub fn returning(self: *Self, _select: _sql.RawQuery) void { pub fn returning(self: *Self, _select: _sql.SqlParams) void {
self.updateConfig.returning = _select; self.updateConfig.returning = _select;
} }
/// Set selected columns for RETURNING clause. /// Set selected columns for RETURNING clause.
pub fn returningColumns(self: *Self, _select: []const []const u8) !void { pub fn returningColumns(self: *Self, _select: []const []const u8) void {
if (_select.len == 0) { if (_select.len == 0) {
return ZrmError.AtLeastOneSelectionRequired; return errors.AtLeastOneSelectionRequired;
} }
self.returning(.{ self.returning(.{
// Join selected columns. // Join selected columns.
.sql = try std.mem.join(self.arena.allocator(), ", ", _select), .sql = std.mem.join(self.arena.allocator(), ", ", _select),
.params = &[_]_sql.RawQueryParameter{}, // No parameters. .params = &[_]_sql.QueryParameter{}, // No parameters.
}); });
} }
@ -169,7 +163,7 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
pub fn returningAll(self: *Self) void { pub fn returningAll(self: *Self) void {
self.returning(.{ self.returning(.{
.sql = "*", .sql = "*",
.params = &[_]_sql.RawQueryParameter{}, // No parameters. .params = &[_]_sql.QueryParameter{}, // No parameters.
}); });
} }
@ -177,7 +171,7 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
pub fn buildSql(self: *Self) !void { pub fn buildSql(self: *Self) !void {
if (self.updateConfig.value) |_| {} else { if (self.updateConfig.value) |_| {} else {
// Updated values must be set. // Updated values must be set.
return ZrmError.UpdatedValuesRequired; return errors.ZrmError.UpdatedValuesRequired;
} }
// Start parameter counter at 1. // Start parameter counter at 1.
@ -207,71 +201,80 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
} }
// Allocate SQL buffer from computed size. // Allocate SQL buffer from computed size.
var sqlBuf = try std.ArrayList(u8).initCapacity(self.arena.allocator(), fixedSqlSize const sqlBuf = try self.arena.allocator().alloc(u8, fixedSqlSize
+ (setSize) + (setSize)
+ (whereSize) + (whereSize)
+ (returningSize) + (returningSize)
); );
defer sqlBuf.deinit();
// Fill SQL buffer.
// Restart parameter counter at 1. // Restart parameter counter at 1.
currentParameter = 1; currentParameter = 1;
// SQL query initialisation. // SQL query initialisation.
try sqlBuf.appendSlice(sqlBase); @memcpy(sqlBuf[0..sqlBase.len], sqlBase);
var sqlBufCursor: usize = sqlBase.len;
// Add SET columns values. // Add SET columns values.
inline for (columns) |column| { inline for (columns) |column| {
// Create the SET string and append it to the SQL buffer. // Create the SET string and append it to the SQL buffer.
try sqlBuf.writer().print("{s}=${d},", .{column, currentParameter}); const setColumnSize = column.len + 1 + 1 + try _sql.computeRequiredSpaceForParameter(currentParameter) + 1;
_ = try std.fmt.bufPrint(sqlBuf[sqlBufCursor..sqlBufCursor+setColumnSize], "{s}=${d},", .{column, currentParameter});
sqlBufCursor += setColumnSize;
// Increment parameter count. // Increment parameter count.
currentParameter += 1; currentParameter += 1;
} }
// Overwrite the last ','. // Overwrite the last ','.
_ = sqlBuf.pop(); sqlBufCursor -= 1;
// WHERE clause. // WHERE clause.
if (self.updateConfig.where) |_where| { if (self.updateConfig.where) |_where| {
try sqlBuf.appendSlice(" " ++ whereClause ++ " "); @memcpy(sqlBuf[sqlBufCursor..sqlBufCursor+(1 + whereClause.len + 1)], " " ++ whereClause ++ " ");
// Copy WHERE clause content and replace parameters, if there are some. // Copy WHERE clause content and replace parameters, if there are some.
try _sql.copyAndReplaceSqlParameters(&currentParameter, try _sql.copyAndReplaceSqlParameters(&currentParameter,
_where.params.len, sqlBuf.writer(), _where.sql _where.params.len,
sqlBuf[sqlBufCursor+(1+whereClause.len+1)..sqlBufCursor+whereSize], _where.sql
); );
sqlBufCursor += whereSize;
} }
// Append RETURNING clause, if there is one defined. // Append RETURNING clause, if there is one defined.
if (self.updateConfig.returning) |_returning| { if (self.updateConfig.returning) |_returning| {
try sqlBuf.appendSlice(" " ++ returningClause ++ " "); @memcpy(sqlBuf[sqlBufCursor..sqlBufCursor+(1 + returningClause.len + 1)], " " ++ returningClause ++ " ");
// Copy RETURNING clause content and replace parameters, if there are some. // Copy RETURNING clause content and replace parameters, if there are some.
try _sql.copyAndReplaceSqlParameters(&currentParameter, try _sql.copyAndReplaceSqlParameters(&currentParameter,
_returning.params.len, sqlBuf.writer(), _returning.sql _returning.params.len,
sqlBuf[sqlBufCursor+(1+returningClause.len+1)..sqlBufCursor+returningSize], _returning.sql
); );
sqlBufCursor += returningSize;
} }
// ";" to end the query. // ";" to end the query.
try sqlBuf.append(';'); sqlBuf[sqlBufCursor] = ';'; sqlBufCursor += 1;
// Save built SQL query. // Save built SQL query.
self.sql = try sqlBuf.toOwnedSlice(); self.sql = sqlBuf;
} }
/// Execute the update query. /// Execute the update query.
fn execQuery(self: *Self) !*pg.Result { fn execQuery(self: *Self) !*pg.Result {
// Get a connection to the database. // Get a connection to the database.
self.connection = try self.connector.getConnection(); const connection = try self.database.acquire();
errdefer self.connection.release(); errdefer connection.release();
// Initialize a new PostgreSQL statement. // Initialize a new PostgreSQL statement.
var statement = try pg.Stmt.init(self.connection.connection, .{ var statement = try pg.Stmt.init(connection, .{
.column_names = true, .column_names = true,
.release_conn = true,
.allocator = self.arena.allocator(), .allocator = self.arena.allocator(),
}); });
errdefer statement.deinit(); errdefer statement.deinit();
// Prepare SQL update query. // Prepare SQL update query.
statement.prepare(self.sql.?) statement.prepare(self.sql.?)
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement); catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
// Bind UPDATE query parameters. // Bind UPDATE query parameters.
inline for (columns) |column| { inline for (columns) |column| {
@ -288,7 +291,7 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
// Execute the query and get its result. // Execute the query and get its result.
const result = statement.execute() const result = statement.execute()
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement); catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
// Query executed successfully, return the result. // Query executed successfully, return the result.
return result; return result;
@ -301,20 +304,40 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
// Execute query and get its result. // Execute query and get its result.
var queryResult = try self.execQuery(); var queryResult = try self.execQuery();
defer self.connection.release();
defer queryResult.deinit(); defer queryResult.deinit();
// Map query results. //TODO deduplicate this in postgresql.zig, we could do it if Mapper type was exposed.
var postgresqlReader = postgresql.QueryResultReader(TableShape, null, null).init(queryResult); //TODO make a generic mapper and do it in repository.zig?
return try ResultMapper.map(false, allocator, self.connector, postgresqlReader.reader()); // Create an arena for mapper data.
var mapperArena = std.heap.ArenaAllocator.init(allocator);
// Get result mapper.
const mapper = queryResult.mapper(TableShape, .{ .allocator = mapperArena.allocator() });
// Initialize models list.
var models = std.ArrayList(*Model).init(allocator);
defer models.deinit();
// Get all raw models from the result mapper.
while (try mapper.next()) |rawModel| {
// Parse each raw model from the mapper.
const model = try allocator.create(Model);
model.* = try repositoryConfig.fromSql(rawModel);
try models.append(model);
}
// Return a result with the models.
return repository.RepositoryResult(Model).init(allocator,
zollections.Collection(Model).init(allocator, try models.toOwnedSlice()),
mapperArena,
);
} }
/// Initialize a new repository update query. /// Initialize a new repository update query.
pub fn init(allocator: std.mem.Allocator, connector: database.Connector) Self { pub fn init(allocator: std.mem.Allocator, database: *pg.Pool) Self {
return .{ return .{
// Initialize an arena allocator for the update query. // Initialize an arena allocator for the update query.
.arena = std.heap.ArenaAllocator.init(allocator), .arena = std.heap.ArenaAllocator.init(allocator),
.connector = connector, .database = database,
.updateConfig = .{}, .updateConfig = .{},
}; };
} }

View file

@ -17,7 +17,6 @@ fn initDatabase() !void {
.password = "zrm", .password = "zrm",
.database = "zrm", .database = "zrm",
}, },
.size = 1,
}); });
} }
@ -61,14 +60,14 @@ const CompositeModelRepository = zrm.Repository(CompositeModel, CompositeModelTa
// Insert shape used by default for inserts in the repository. // Insert shape used by default for inserts in the repository.
.insertShape = struct { .insertShape = struct {
secondcol: []const u8, secondcol: zrm.Insertable([]const u8),
label: []const u8, label: zrm.Insertable([]const u8),
}, },
.key = &[_][]const u8{"firstcol", "secondcol"}, .key = &[_][]const u8{"firstcol", "secondcol"},
.fromSql = &zrm.helpers.TableModel(CompositeModel, CompositeModelTable).copyTableToModel, .fromSql = &modelFromSql,
.toSql = &zrm.helpers.TableModel(CompositeModel, CompositeModelTable).copyModelToTable, .toSql = &modelToSql,
}); });
@ -77,9 +76,6 @@ test "composite model create, save and find" {
try initDatabase(); try initDatabase();
defer database.deinit(); defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
// Initialize a test model. // Initialize a test model.
var newModel = CompositeModel{ var newModel = CompositeModel{
@ -90,7 +86,7 @@ test "composite model create, save and find" {
// Create the new model. // Create the new model.
var result = try CompositeModelRepository.create(std.testing.allocator, poolConnector.connector(), &newModel); var result = try CompositeModelRepository.create(std.testing.allocator, database, &newModel);
defer result.deinit(); // Will clear some values in newModel. defer result.deinit(); // Will clear some values in newModel.
// Check that the model is correctly defined. // Check that the model is correctly defined.
@ -105,7 +101,7 @@ test "composite model create, save and find" {
// Update the model. // Update the model.
newModel.label = null; newModel.label = null;
var result2 = try CompositeModelRepository.save(std.testing.allocator, poolConnector.connector(), &newModel); var result2 = try CompositeModelRepository.save(std.testing.allocator, database, &newModel);
defer result2.deinit(); // Will clear some values in newModel. defer result2.deinit(); // Will clear some values in newModel.
// Checking that the model has been updated (but only the right field). // Checking that the model has been updated (but only the right field).
@ -115,7 +111,7 @@ test "composite model create, save and find" {
// Do another insert with the same secondcol. // Do another insert with the same secondcol.
var insertQuery = CompositeModelRepository.Insert.init(std.testing.allocator, poolConnector.connector()); var insertQuery = CompositeModelRepository.Insert.init(std.testing.allocator, database);
defer insertQuery.deinit(); defer insertQuery.deinit();
try insertQuery.values(.{ try insertQuery.values(.{
.secondcol = "identifier", .secondcol = "identifier",
@ -132,7 +128,7 @@ test "composite model create, save and find" {
// Try to find the created then saved model, to check that everything has been saved correctly. // Try to find the created then saved model, to check that everything has been saved correctly.
var result4 = try CompositeModelRepository.find(std.testing.allocator, poolConnector.connector(), .{ var result4 = try CompositeModelRepository.find(std.testing.allocator, database, .{
.firstcol = newModel.firstcol, .firstcol = newModel.firstcol,
.secondcol = newModel.secondcol, .secondcol = newModel.secondcol,
}); });
@ -143,7 +139,7 @@ test "composite model create, save and find" {
// Try to find multiple models at once. // Try to find multiple models at once.
var result5 = try CompositeModelRepository.find(std.testing.allocator, poolConnector.connector(), &[_]CompositeModelRepository.KeyType{ var result5 = try CompositeModelRepository.find(std.testing.allocator, database, &[_]CompositeModelRepository.KeyType{
.{ .{
.firstcol = newModel.firstcol, .firstcol = newModel.firstcol,
.secondcol = newModel.secondcol, .secondcol = newModel.secondcol,

View file

@ -1,479 +0,0 @@
const std = @import("std");
const pg = @import("pg");
const zrm = @import("zrm");
/// PostgreSQL database connection.
var database: *pg.Pool = undefined;
/// Initialize database connection.
fn initDatabase(allocator: std.mem.Allocator) !void {
database = try pg.Pool.init(allocator, .{
.connect = .{
.host = "localhost",
.port = 5432,
},
.auth = .{
.username = "zrm",
.password = "zrm",
.database = "zrm",
},
.size = 5,
});
}
pub const Media = struct {
pub const Table = struct {
id: i32,
filename: []const u8,
pub const Insert = struct {
filename: []const u8,
};
};
id: i32,
filename: []const u8,
};
pub const MediaRepository = zrm.Repository(Media, Media.Table, .{
.table = "example_medias",
.insertShape = Media.Table.Insert,
.key = &[_][]const u8{"id"},
.fromSql = zrm.helpers.TableModel(Media, Media.Table).copyTableToModel,
.toSql = zrm.helpers.TableModel(Media, Media.Table).copyModelToTable,
});
pub const User = struct {
pub const Table = struct {
id: i32,
name: []const u8,
picture_id: ?i32,
pub const Insert = struct {
name: []const u8,
picture_id: ?i32,
};
};
id: i32,
name: []const u8,
picture_id: ?i32,
picture: ?Media = null,
info: ?UserInfo = null,
messages: ?[]Message = null,
medias: ?[]Media = null,
};
pub const UserRepository = zrm.Repository(User, User.Table, .{
.table = "example_users",
.insertShape = User.Table.Insert,
.key = &[_][]const u8{"id"},
.fromSql = zrm.helpers.TableModel(User, User.Table).copyTableToModel,
.toSql = zrm.helpers.TableModel(User, User.Table).copyModelToTable,
});
pub const UserRelationships = UserRepository.relationships.define(.{
.picture = UserRepository.relationships.one(MediaRepository, .{
.direct = .{
.foreignKey = "picture_id",
}
}),
.info = UserRepository.relationships.one(UserInfoRepository, .{
.reverse = .{},
}),
.messages = UserRepository.relationships.many(MessageRepository, .{
.direct = .{
.foreignKey = "user_id",
},
}),
//TODO double through to get medias?
});
pub const UserInfo = struct {
pub const Table = struct {
user_id: i32,
birthdate: i64,
pub const Insert = struct {
user_id: i32,
birthdate: i64,
};
};
user_id: i32,
birthdate: i64,
user: ?*User = null,
};
pub const UserInfoRepository = zrm.Repository(UserInfo, UserInfo.Table, .{
.table = "example_users_info",
.insertShape = UserInfo.Table.Insert,
.key = &[_][]const u8{"user_id"},
.fromSql = zrm.helpers.TableModel(UserInfo, UserInfo.Table).copyTableToModel,
.toSql = zrm.helpers.TableModel(UserInfo, UserInfo.Table).copyModelToTable,
});
pub const UserInfoRelationships = UserInfoRepository.relationships.define(.{
.user = UserInfoRepository.relationships.one(UserRepository, .{
.direct = .{
.foreignKey = "user_id",
},
}),
});
pub const Message = struct {
pub const Table = struct {
id: i32,
text: []const u8,
user_id: i32,
pub const Insert = struct {
text: []const u8,
user_id: i32,
};
};
id: i32,
text: []const u8,
user_id: i32,
user: ?User = null,
user_picture: ?Media = null,
medias: ?[]Media = null,
};
pub const MessageRepository = zrm.Repository(Message, Message.Table, .{
.table = "example_messages",
.insertShape = Message.Table.Insert,
.key = &[_][]const u8{"id"},
.fromSql = zrm.helpers.TableModel(Message, Message.Table).copyTableToModel,
.toSql = zrm.helpers.TableModel(Message, Message.Table).copyModelToTable,
});
pub const MessageRelationships = MessageRepository.relationships.define(.{
.user = MessageRepository.relationships.one(UserRepository, .{
.direct = .{
.foreignKey = "user_id",
}
}),
.user_picture = MessageRepository.relationships.one(MediaRepository, .{
.through = .{
.table = "example_users",
.foreignKey = "user_id",
.joinForeignKey = "id",
.joinModelKey = "picture_id",
},
}),
.medias = MessageRepository.relationships.many(MediaRepository, .{
.through = .{
.table = "example_messages_medias",
.joinModelKey = "message_id",
.joinForeignKey = "media_id",
},
}),
});
test "user picture media" {
zrm.setDebug(true);
try initDatabase(std.testing.allocator);
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
var firstQuery = UserRepository.QueryWith(
// Retrieve picture of users.
&[_]zrm.relationships.Relationship{UserRelationships.picture}
).init(std.testing.allocator, poolConnector.connector(), .{});
try firstQuery.whereKey(1);
defer firstQuery.deinit();
var firstResult = try firstQuery.get(std.testing.allocator);
defer firstResult.deinit();
try std.testing.expectEqual(1, firstResult.models.len);
try std.testing.expectEqual(1, firstResult.models[0].picture_id);
try std.testing.expectEqual(1, firstResult.models[0].picture.?.id);
try std.testing.expectEqualStrings("profile.jpg", firstResult.models[0].picture.?.filename);
var secondQuery = UserRepository.QueryWith(
// Retrieve picture of users.
&[_]zrm.relationships.Relationship{UserRelationships.picture}
).init(std.testing.allocator, poolConnector.connector(), .{});
try secondQuery.whereKey(3);
defer secondQuery.deinit();
var secondResult = try secondQuery.get(std.testing.allocator);
defer secondResult.deinit();
try std.testing.expectEqual(1, secondResult.models.len);
try std.testing.expectEqual(2, secondResult.models[0].picture_id);
try std.testing.expectEqual(2, secondResult.models[0].picture.?.id);
try std.testing.expectEqualStrings("profile.png", secondResult.models[0].picture.?.filename);
var thirdQuery = UserRepository.QueryWith(
// Retrieve picture of users.
&[_]zrm.relationships.Relationship{UserRelationships.picture}
).init(std.testing.allocator, poolConnector.connector(), .{});
try thirdQuery.whereKey(5);
defer thirdQuery.deinit();
var thirdResult = try thirdQuery.get(std.testing.allocator);
defer thirdResult.deinit();
try std.testing.expectEqual(1, thirdResult.models.len);
try std.testing.expectEqual(null, thirdResult.models[0].picture_id);
try std.testing.expectEqual(null, thirdResult.models[0].picture);
var fourthQuery = UserRepository.QueryWith(
// Retrieve picture of users.
&[_]zrm.relationships.Relationship{UserRelationships.picture}
).init(std.testing.allocator, poolConnector.connector(), .{});
try fourthQuery.whereKey(19);
defer fourthQuery.deinit();
var fourthResult = try fourthQuery.get(std.testing.allocator);
defer fourthResult.deinit();
try std.testing.expectEqual(0, fourthResult.models.len);
}
test "user has info" {
zrm.setDebug(true);
try initDatabase(std.testing.allocator);
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
var firstQuery = UserRepository.QueryWith(
// Retrieve info of users.
&[_]zrm.relationships.Relationship{UserRelationships.info}
).init(std.testing.allocator, poolConnector.connector(), .{});
try firstQuery.whereKey(2);
defer firstQuery.deinit();
var firstResult = try firstQuery.get(std.testing.allocator);
defer firstResult.deinit();
try std.testing.expectEqual(1, firstResult.models.len);
try std.testing.expect(firstResult.models[0].info != null);
try std.testing.expectEqual(2, firstResult.models[0].info.?.user_id);
try std.testing.expectEqual(876348000000000, firstResult.models[0].info.?.birthdate);
var secondQuery = UserRepository.QueryWith(
// Retrieve info of users.
&[_]zrm.relationships.Relationship{UserRelationships.info}
).init(std.testing.allocator, poolConnector.connector(), .{});
try secondQuery.whereKey(1);
defer secondQuery.deinit();
var secondResult = try secondQuery.get(std.testing.allocator);
defer secondResult.deinit();
try std.testing.expectEqual(1, secondResult.models.len);
try std.testing.expect(secondResult.models[0].info == null);
var thirdQuery = UserInfoRepository.QueryWith(
// Retrieve info of users.
&[_]zrm.relationships.Relationship{UserInfoRelationships.user}
).init(std.testing.allocator, poolConnector.connector(), .{});
try thirdQuery.whereKey(2);
defer thirdQuery.deinit();
var thirdResult = try thirdQuery.get(std.testing.allocator);
defer thirdResult.deinit();
try std.testing.expectEqual(1, thirdResult.models.len);
try std.testing.expectEqual(876348000000000, thirdResult.models[0].birthdate);
try std.testing.expect(thirdResult.models[0].user != null);
try std.testing.expectEqual(2, thirdResult.models[0].user.?.id);
try std.testing.expectEqualStrings("madeorsk", thirdResult.models[0].user.?.name);
}
test "user has many messages" {
zrm.setDebug(true);
try initDatabase(std.testing.allocator);
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
var firstQuery = UserRepository.QueryWith(
// Retrieve messages of users.
&[_]zrm.relationships.Relationship{UserRelationships.messages}
).init(std.testing.allocator, poolConnector.connector(), .{});
try firstQuery.whereKey(1);
defer firstQuery.deinit();
var firstResult = try firstQuery.get(std.testing.allocator);
defer firstResult.deinit();
try std.testing.expectEqual(1, firstResult.models.len);
try std.testing.expect(firstResult.models[0].messages != null);
try std.testing.expectEqual(3, firstResult.models[0].messages.?.len);
for (firstResult.models[0].messages.?) |message| {
if (message.id == 2) {
try std.testing.expectEqualStrings("I want to test something.", message.text);
} else if (message.id == 3) {
try std.testing.expectEqualStrings("Lorem ipsum dolor sit amet", message.text);
} else if (message.id == 6) {
try std.testing.expectEqualStrings("foo bar baz", message.text);
} else {
try std.testing.expect(false);
}
}
var secondQuery = UserRepository.QueryWith(
// Retrieve messages of users.
&[_]zrm.relationships.Relationship{UserRelationships.messages}
).init(std.testing.allocator, poolConnector.connector(), .{});
try secondQuery.whereKey(5);
defer secondQuery.deinit();
var secondResult = try secondQuery.get(std.testing.allocator);
defer secondResult.deinit();
try std.testing.expectEqual(1, secondResult.models.len);
try std.testing.expect(secondResult.models[0].messages != null);
try std.testing.expectEqual(0, secondResult.models[0].messages.?.len);
}
test "message has many medias through pivot table" {
zrm.setDebug(true);
try initDatabase(std.testing.allocator);
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
var firstQuery = MessageRepository.QueryWith(
// Retrieve medias of messages.
&[_]zrm.relationships.Relationship{MessageRelationships.medias}
).init(std.testing.allocator, poolConnector.connector(), .{});
try firstQuery.whereKey(1);
defer firstQuery.deinit();
var firstResult = try firstQuery.get(std.testing.allocator);
defer firstResult.deinit();
try std.testing.expectEqual(1, firstResult.models.len);
try std.testing.expect(firstResult.models[0].medias != null);
try std.testing.expectEqual(1, firstResult.models[0].medias.?.len);
try std.testing.expectEqualStrings("attachment.png", firstResult.models[0].medias.?[0].filename);
var secondQuery = MessageRepository.QueryWith(
// Retrieve medias of messages.
&[_]zrm.relationships.Relationship{MessageRelationships.medias}
).init(std.testing.allocator, poolConnector.connector(), .{});
try secondQuery.whereKey(6);
defer secondQuery.deinit();
var secondResult = try secondQuery.get(std.testing.allocator);
defer secondResult.deinit();
try std.testing.expectEqual(1, secondResult.models.len);
try std.testing.expect(secondResult.models[0].medias != null);
try std.testing.expectEqual(2, secondResult.models[0].medias.?.len);
for (secondResult.models[0].medias.?) |media| {
if (media.id == 3) {
try std.testing.expectEqualStrings("attachment.png", media.filename);
} else if (media.id == 5) {
try std.testing.expectEqualStrings("music.opus", media.filename);
} else {
try std.testing.expect(false);
}
}
var thirdQuery = MessageRepository.QueryWith(
// Retrieve medias of messages.
&[_]zrm.relationships.Relationship{MessageRelationships.medias}
).init(std.testing.allocator, poolConnector.connector(), .{});
try thirdQuery.whereKey(4);
defer thirdQuery.deinit();
var thirdResult = try thirdQuery.get(std.testing.allocator);
defer thirdResult.deinit();
try std.testing.expectEqual(1, thirdResult.models.len);
try std.testing.expect(thirdResult.models[0].medias != null);
try std.testing.expectEqualStrings("Je pense donc je suis", thirdResult.models[0].text);
try std.testing.expectEqual(0, thirdResult.models[0].medias.?.len);
}
test "message has one user picture URL through users table" {
zrm.setDebug(true);
try initDatabase(std.testing.allocator);
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
var firstQuery = MessageRepository.QueryWith(
// Retrieve user pictures of messages.
&[_]zrm.relationships.Relationship{MessageRelationships.user_picture}
).init(std.testing.allocator, poolConnector.connector(), .{});
try firstQuery.whereKey(1);
defer firstQuery.deinit();
var firstResult = try firstQuery.get(std.testing.allocator);
defer firstResult.deinit();
try std.testing.expectEqual(1, firstResult.models.len);
try std.testing.expect(firstResult.models[0].user_picture != null);
try std.testing.expectEqualStrings("profile.jpg", firstResult.models[0].user_picture.?.filename);
var secondQuery = MessageRepository.QueryWith(
// Retrieve user pictures of messages.
&[_]zrm.relationships.Relationship{MessageRelationships.user_picture}
).init(std.testing.allocator, poolConnector.connector(), .{});
try secondQuery.whereKey(4);
defer secondQuery.deinit();
var secondResult = try secondQuery.get(std.testing.allocator);
defer secondResult.deinit();
try std.testing.expectEqual(1, secondResult.models.len);
try std.testing.expect(secondResult.models[0].user_picture == null);
}
//TODO try to load all one relationships types in another query (with buildQuery).

View file

@ -2,95 +2,20 @@
DROP SCHEMA public CASCADE; DROP SCHEMA public CASCADE;
CREATE SCHEMA public; CREATE SCHEMA public;
-- Create models table. -- Create default models table.
CREATE TABLE models ( CREATE TABLE models (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
amount NUMERIC(12, 2) NOT NULL amount NUMERIC(12, 2) NOT NULL
); );
-- Create submodels table.
CREATE TABLE submodels (
uuid UUID PRIMARY KEY,
label VARCHAR NOT NULL,
parent_id INT NULL,
FOREIGN KEY (parent_id) REFERENCES models ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE INDEX submodels_parent_id_index ON submodels(parent_id);
-- Insert default data. -- Insert default data.
INSERT INTO models(name, amount) VALUES ('test', 50); INSERT INTO models(name, amount) VALUES ('test', 50);
INSERT INTO models(name, amount) VALUES ('updatable', 33.12);
INSERT INTO submodels(uuid, label, parent_id) VALUES ('f6868a5b-2efc-455f-b76e-872df514404f', 'test', 1);
INSERT INTO submodels(uuid, label, parent_id) VALUES ('013ef171-9781-40e9-b843-f6bc11890070', 'another', 1);
-- Create composite models table. -- Create default composite models table.
CREATE TABLE composite_models ( CREATE TABLE composite_models (
firstcol SERIAL NOT NULL, firstcol SERIAL NOT NULL,
secondcol VARCHAR NOT NULL, secondcol VARCHAR NOT NULL,
label VARCHAR NULL, label VARCHAR NULL,
PRIMARY KEY (firstcol, secondcol) PRIMARY KEY (firstcol, secondcol)
); );
-- Create example models.
CREATE TABLE example_medias (
id SERIAL PRIMARY KEY,
filename VARCHAR NOT NULL
);
CREATE TABLE example_users (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
picture_id INT
);
CREATE TABLE example_users_info (
user_id INT PRIMARY KEY,
birthdate TIMESTAMP WITH TIME ZONE NOT NULL
);
CREATE TABLE example_messages (
id SERIAL PRIMARY KEY,
text TEXT NOT NULL,
user_id INT NOT NULL
);
CREATE TABLE example_messages_medias (
message_id INT NOT NULL,
media_id INT NOT NULL,
PRIMARY KEY (message_id, media_id)
);
-- Fill example models.
INSERT INTO example_medias (filename) VALUES ('profile.jpg'), ('profile.png'), ('attachment.png'), ('video.mp4'), ('music.opus');
INSERT INTO example_users (name, picture_id) VALUES
('test', 1),
('madeorsk', 1),
('foo', 2),
('bar', NULL),
('baz', NULL);
INSERT INTO example_users_info (user_id, birthdate) VALUES
(2, '1997-10-09');
INSERT INTO example_messages (text, user_id) VALUES
('this is a test', 2),
('I want to test something.', 1),
('Lorem ipsum dolor sit amet', 1),
('Je pense donc je suis', 4),
('The quick brown fox jumps over the lazy dog', 3),
('foo bar baz', 1),
('How are you?', 2),
('Fine!', 3);
INSERT INTO example_messages_medias (message_id, media_id) VALUES
(1, 3),
(2, 4),
(6, 3),
(6, 5),
(8, 2);

View file

@ -39,9 +39,9 @@ test "zrm.conditions combined" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit(); defer arena.deinit();
const condition = try zrm.conditions.@"and"(arena.allocator(), &[_]zrm.RawQuery{ const condition = try zrm.conditions.@"and"(arena.allocator(), &[_]zrm.SqlParams{
try zrm.conditions.value(usize, arena.allocator(), "test", "=", 5), try zrm.conditions.value(usize, arena.allocator(), "test", "=", 5),
try zrm.conditions.@"or"(arena.allocator(), &[_]zrm.RawQuery{ try zrm.conditions.@"or"(arena.allocator(), &[_]zrm.SqlParams{
try zrm.conditions.in(usize, arena.allocator(), "intest", &[_]usize{2, 3, 8}), try zrm.conditions.in(usize, arena.allocator(), "intest", &[_]usize{2, 3, 8}),
try zrm.conditions.column(arena.allocator(), "firstcol", "<>", "secondcol"), try zrm.conditions.column(arena.allocator(), "firstcol", "<>", "secondcol"),
}), }),

View file

@ -1,100 +0,0 @@
const std = @import("std");
const pg = @import("pg");
const zrm = @import("zrm");
const repository = @import("repository.zig");
/// PostgreSQL database connection.
var database: *pg.Pool = undefined;
/// Initialize database connection.
fn initDatabase(allocator: std.mem.Allocator) !void {
database = try pg.Pool.init(allocator, .{
.connect = .{
.host = "localhost",
.port = 5432,
},
.auth = .{
.username = "zrm",
.password = "zrm",
.database = "zrm",
},
.size = 5,
});
}
test "belongsTo" {
zrm.setDebug(true);
try initDatabase(std.testing.allocator);
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
// Build a query of submodels.
var myQuery = repository.MySubmodelRepository.QueryWith(
// Retrieve parents of submodels from relationship.
&[_]zrm.relationships.Relationship{repository.MySubmodelRelationships.parent}
).init(std.testing.allocator, poolConnector.connector(), .{});
defer myQuery.deinit();
try myQuery.buildSql();
// Get query result.
var result = try myQuery.get(std.testing.allocator);
defer result.deinit();
// Checking result.
try std.testing.expectEqual(2, result.models.len);
try std.testing.expectEqual(1, result.models[0].parent_id);
try std.testing.expectEqual(1, result.models[1].parent_id);
try std.testing.expectEqual(repository.MyModel, @TypeOf(result.models[0].parent.?));
try std.testing.expectEqual(1, result.models[0].parent.?.id);
try std.testing.expectEqual(1, result.models[1].parent.?.id);
}
test "hasMany" {
zrm.setDebug(true);
try initDatabase(std.testing.allocator);
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
// Build a query of submodels.
var myQuery = repository.MyModelRepository.QueryWith(
// Retrieve parents of submodels from relationship.
&[_]zrm.relationships.Relationship{repository.MyModelRelationships.submodels}
).init(std.testing.allocator, poolConnector.connector(), .{});
defer myQuery.deinit();
try myQuery.buildSql();
// Get query result.
var result = try myQuery.get(std.testing.allocator);
defer result.deinit();
// Checking result.
try std.testing.expectEqual(repository.MySubmodel, @TypeOf(result.models[0].submodels.?[0]));
// Checking retrieved submodels.
for (result.models) |model| {
try std.testing.expect(model.submodels != null);
if (model.submodels.?.len > 0) {
try std.testing.expectEqual(1, model.id);
try std.testing.expectEqual(2, model.submodels.?.len);
for (model.submodels.?) |submodel| {
try std.testing.expectEqual(1, submodel.parent_id.?);
try std.testing.expect(
std.mem.eql(u8, &try pg.uuidToHex(submodel.uuid), "f6868a5b-2efc-455f-b76e-872df514404f")
or std.mem.eql(u8, &try pg.uuidToHex(submodel.uuid), "013ef171-9781-40e9-b843-f6bc11890070")
);
try std.testing.expect(
std.mem.eql(u8, submodel.label, "test") or std.mem.eql(u8, submodel.label, "another")
);
}
}
}
}

View file

@ -17,67 +17,19 @@ fn initDatabase() !void {
.password = "zrm", .password = "zrm",
.database = "zrm", .database = "zrm",
}, },
.size = 1,
}); });
} }
/// An example submodel, child of the example model. /// An example submodel, child of the example model.
pub const MySubmodel = struct { const MySubmodel = struct {
uuid: []const u8, uuid: []const u8,
label: []const u8, label: []const u8,
parent_id: ?i32 = null,
parent: ?MyModel = null, parent: ?MyModel = null,
}; };
const MySubmodelTable = struct {
uuid: []const u8,
label: []const u8,
parent_id: ?i32 = null,
};
// Convert an SQL row to a model.
fn submodelFromSql(raw: MySubmodelTable) !MySubmodel {
return .{
.uuid = raw.uuid,
.label = raw.label,
.parent_id = raw.parent_id,
};
}
/// Convert a model to an SQL row.
fn submodelToSql(model: MySubmodel) !MySubmodelTable {
return .{
.uuid = model.uuid,
.label = model.label,
.parent_id = model.parent_id,
};
}
/// Declare a model repository.
pub const MySubmodelRepository = zrm.Repository(MySubmodel, MySubmodelTable, .{
.table = "submodels",
// Insert shape used by default for inserts in the repository.
.insertShape = MySubmodelTable,
.key = &[_][]const u8{"uuid"},
.fromSql = &submodelFromSql,
.toSql = &submodelToSql,
});
pub const MySubmodelRelationships = MySubmodelRepository.relationships.define(.{
.parent = MySubmodelRepository.relationships.one(MyModelRepository, .{
.direct = .{
.foreignKey = "parent_id",
},
}),
});
/// An example model. /// An example model.
pub const MyModel = struct { const MyModel = struct {
id: i32, id: i32,
name: []const u8, name: []const u8,
amount: f64, amount: f64,
@ -86,7 +38,7 @@ pub const MyModel = struct {
}; };
/// SQL table shape of the example model. /// SQL table shape of the example model.
pub const MyModelTable = struct { const MyModelTable = struct {
id: i32, id: i32,
name: []const u8, name: []const u8,
amount: f64, amount: f64,
@ -111,13 +63,13 @@ fn modelToSql(model: MyModel) !MyModelTable {
} }
/// Declare a model repository. /// Declare a model repository.
pub const MyModelRepository = zrm.Repository(MyModel, MyModelTable, .{ const MyModelRepository = zrm.Repository(MyModel, MyModelTable, .{
.table = "models", .table = "models",
// Insert shape used by default for inserts in the repository. // Insert shape used by default for inserts in the repository.
.insertShape = struct { .insertShape = struct {
name: []const u8, name: zrm.Insertable([]const u8),
amount: f64, amount: zrm.Insertable(f64),
}, },
.key = &[_][]const u8{"id"}, .key = &[_][]const u8{"id"},
@ -126,14 +78,6 @@ pub const MyModelRepository = zrm.Repository(MyModel, MyModelTable, .{
.toSql = &modelToSql, .toSql = &modelToSql,
}); });
pub const MyModelRelationships = MyModelRepository.relationships.define(.{
.submodels = MyModelRepository.relationships.many(MySubmodelRepository, .{
.direct = .{
.foreignKey = "parent_id",
}
}),
});
test "model structures" { test "model structures" {
// Initialize a test model. // Initialize a test model.
@ -161,16 +105,13 @@ test "repository query SQL builder" {
try initDatabase(); try initDatabase();
defer database.deinit(); defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
var query = MyModelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{}); var query = MyModelRepository.Query.init(std.testing.allocator, database, .{});
defer query.deinit(); defer query.deinit();
try query.whereIn(usize, "id", &[_]usize{1, 2}); try query.whereIn(usize, "id", &[_]usize{1, 2});
try query.buildSql(); try query.buildSql();
const expectedSql = "SELECT \"models\".* FROM \"models\" WHERE id IN ($1,$2);"; const expectedSql = "SELECT * FROM models WHERE id IN ($1,$2);";
try std.testing.expectEqual(expectedSql.len, query.sql.?.len); try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
try std.testing.expectEqualStrings(expectedSql, query.sql.?); try std.testing.expectEqualStrings(expectedSql, query.sql.?);
} }
@ -180,12 +121,9 @@ test "repository element retrieval" {
try initDatabase(); try initDatabase();
defer database.deinit(); defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
// Prepare a query for models. // Prepare a query for models.
var query = MyModelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{}); var query = MyModelRepository.Query.init(std.testing.allocator, database, .{});
try query.whereValue(usize, "id", "=", 1); try query.whereValue(usize, "id", "=", 1);
defer query.deinit(); defer query.deinit();
@ -193,7 +131,7 @@ test "repository element retrieval" {
try query.buildSql(); try query.buildSql();
// Check built SQL. // Check built SQL.
const expectedSql = "SELECT \"models\".* FROM \"models\" WHERE id = $1;"; const expectedSql = "SELECT * FROM models WHERE id = $1;";
try std.testing.expectEqual(expectedSql.len, query.sql.?.len); try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
try std.testing.expectEqualStrings(expectedSql, query.sql.?); try std.testing.expectEqualStrings(expectedSql, query.sql.?);
@ -214,18 +152,15 @@ test "repository complex SQL query" {
try initDatabase(); try initDatabase();
defer database.deinit(); defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
var query = MyModelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{}); var query = MyModelRepository.Query.init(std.testing.allocator, database, .{});
defer query.deinit(); defer query.deinit();
query.where( query.where(
try query.newCondition().@"or"(&[_]zrm.RawQuery{ try query.newCondition().@"or"(&[_]zrm.SqlParams{
try query.newCondition().value(usize, "id", "=", 1), try query.newCondition().value(usize, "id", "=", 1),
try query.newCondition().@"and"(&[_]zrm.RawQuery{ try query.newCondition().@"and"(&[_]zrm.SqlParams{
try query.newCondition().in(usize, "id", &[_]usize{100000, 200000, 300000}), try query.newCondition().in(usize, "id", &[_]usize{100000, 200000, 300000}),
try query.newCondition().@"or"(&[_]zrm.RawQuery{ try query.newCondition().@"or"(&[_]zrm.SqlParams{
try query.newCondition().value(f64, "amount", ">", 12.13), try query.newCondition().value(f64, "amount", ">", 12.13),
try query.newCondition().value([]const u8, "name", "=", "test"), try query.newCondition().value([]const u8, "name", "=", "test"),
}) })
@ -234,7 +169,7 @@ test "repository complex SQL query" {
); );
try query.buildSql(); try query.buildSql();
const expectedSql = "SELECT \"models\".* FROM \"models\" WHERE (id = $1 OR (id IN ($2,$3,$4) AND (amount > $5 OR name = $6)));"; const expectedSql = "SELECT * FROM models WHERE (id = $1 OR (id IN ($2,$3,$4) AND (amount > $5 OR name = $6)));";
try std.testing.expectEqual(expectedSql.len, query.sql.?.len); try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
try std.testing.expectEqualStrings(expectedSql, query.sql.?); try std.testing.expectEqualStrings(expectedSql, query.sql.?);
@ -252,9 +187,6 @@ test "repository element creation" {
try initDatabase(); try initDatabase();
defer database.deinit(); defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
// Create a model to insert. // Create a model to insert.
const newModel = MyModel{ const newModel = MyModel{
@ -264,7 +196,7 @@ test "repository element creation" {
}; };
// Initialize an insert query. // Initialize an insert query.
var insertQuery = MyModelRepository.Insert.init(std.testing.allocator, poolConnector.connector()); var insertQuery = MyModelRepository.Insert.init(std.testing.allocator, database);
defer insertQuery.deinit(); defer insertQuery.deinit();
// Insert the new model. // Insert the new model.
try insertQuery.values(newModel); try insertQuery.values(newModel);
@ -293,19 +225,16 @@ test "repository element update" {
try initDatabase(); try initDatabase();
defer database.deinit(); defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
// Initialize an update query. // Initialize an update query.
var updateQuery = MyModelRepository.Update(struct { var updateQuery = MyModelRepository.Update(struct {
name: []const u8, name: []const u8,
}).init(std.testing.allocator, poolConnector.connector()); }).init(std.testing.allocator, database);
defer updateQuery.deinit(); defer updateQuery.deinit();
// Update a model's name. // Update a model's name.
try updateQuery.set(.{ .name = "newname" }); try updateQuery.set(.{ .name = "newname" });
try updateQuery.whereValue(usize, "id", "=", 2); try updateQuery.whereValue(usize, "id", "=", 1);
updateQuery.returningAll(); updateQuery.returningAll();
// Build SQL. // Build SQL.
@ -322,7 +251,7 @@ test "repository element update" {
// Check the updated model. // Check the updated model.
try std.testing.expectEqual(1, result.models.len); try std.testing.expectEqual(1, result.models.len);
try std.testing.expectEqual(2, result.models[0].id); try std.testing.expectEqual(1, result.models[0].id);
try std.testing.expectEqualStrings("newname", result.models[0].name); try std.testing.expectEqualStrings("newname", result.models[0].name);
} }
@ -331,9 +260,6 @@ test "model create, save and find" {
try initDatabase(); try initDatabase();
defer database.deinit(); defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
// Initialize a test model. // Initialize a test model.
var newModel = MyModel{ var newModel = MyModel{
@ -344,7 +270,7 @@ test "model create, save and find" {
// Create the new model. // Create the new model.
var result = try MyModelRepository.create(std.testing.allocator, poolConnector.connector(), &newModel); var result = try MyModelRepository.create(std.testing.allocator, database, &newModel);
defer result.deinit(); // Will clear some values in newModel. defer result.deinit(); // Will clear some values in newModel.
// Check that the model is correctly defined. // Check that the model is correctly defined.
@ -358,7 +284,7 @@ test "model create, save and find" {
// Update the model. // Update the model.
newModel.name = "recently updated name"; newModel.name = "recently updated name";
var result2 = try MyModelRepository.save(std.testing.allocator, poolConnector.connector(), &newModel); var result2 = try MyModelRepository.save(std.testing.allocator, database, &newModel);
defer result2.deinit(); // Will clear some values in newModel. defer result2.deinit(); // Will clear some values in newModel.
// Checking that the model has been updated (but only the right field). // Checking that the model has been updated (but only the right field).
@ -370,7 +296,7 @@ test "model create, save and find" {
// Do another update. // Do another update.
newModel.amount = 12.226; newModel.amount = 12.226;
var result3 = try MyModelRepository.save(std.testing.allocator, poolConnector.connector(), &newModel); var result3 = try MyModelRepository.save(std.testing.allocator, database, &newModel);
defer result3.deinit(); // Will clear some values in newModel. defer result3.deinit(); // Will clear some values in newModel.
// Checking that the model has been updated (but only the right field). // Checking that the model has been updated (but only the right field).
@ -380,14 +306,14 @@ test "model create, save and find" {
// Try to find the created then saved model, to check that everything has been saved correctly. // Try to find the created then saved model, to check that everything has been saved correctly.
var result4 = try MyModelRepository.find(std.testing.allocator, poolConnector.connector(), newModel.id); var result4 = try MyModelRepository.find(std.testing.allocator, database, newModel.id);
defer result4.deinit(); // Will clear some values in newModel. defer result4.deinit(); // Will clear some values in newModel.
try std.testing.expectEqualDeep(newModel, result4.first().?.*); try std.testing.expectEqualDeep(newModel, result4.first().?.*);
// Try to find multiple models at once. // Try to find multiple models at once.
var result5 = try MyModelRepository.find(std.testing.allocator, poolConnector.connector(), &[_]i32{1, newModel.id}); var result5 = try MyModelRepository.find(std.testing.allocator, database, &[_]i32{1, newModel.id});
defer result5.deinit(); defer result5.deinit();
try std.testing.expectEqual(2, result5.models.len); try std.testing.expectEqual(2, result5.models.len);

View file

@ -4,7 +4,4 @@ comptime {
_ = @import("query.zig"); _ = @import("query.zig");
_ = @import("repository.zig"); _ = @import("repository.zig");
_ = @import("composite.zig"); _ = @import("composite.zig");
_ = @import("sessions.zig");
_ = @import("relationships.zig");
_ = @import("example.zig");
} }

View file

@ -1,134 +0,0 @@
const std = @import("std");
const pg = @import("pg");
const zrm = @import("zrm");
const repository = @import("repository.zig");
/// PostgreSQL database connection.
var database: *pg.Pool = undefined;
/// Initialize database connection.
fn initDatabase(allocator: std.mem.Allocator) !void {
database = try pg.Pool.init(allocator, .{
.connect = .{
.host = "localhost",
.port = 5432,
},
.auth = .{
.username = "zrm",
.password = "zrm",
.database = "zrm",
},
.size = 1,
});
}
test "session with rolled back transaction and savepoint" {
zrm.setDebug(true);
// Initialize database.
try initDatabase(std.testing.allocator);
defer database.deinit();
// Start a new session and perform operations in a transaction.
var session = try zrm.Session.init(database);
defer session.deinit();
try session.beginTransaction();
// First UPDATE in the transaction.
{
var firstUpdate = repository.MyModelRepository.Update(struct {
name: []const u8,
}).init(std.testing.allocator, session.connector());
defer firstUpdate.deinit();
try firstUpdate.set(.{
.name = "tempname",
});
try firstUpdate.whereValue(usize, "id", "=", 1);
var firstUpdateResult = try firstUpdate.update(std.testing.allocator);
firstUpdateResult.deinit();
}
// Set a savepoint.
try session.savepoint("my_savepoint");
// Second UPDATE in the transaction.
{
var secondUpdate = repository.MyModelRepository.Update(struct {
amount: f64,
}).init(std.testing.allocator, session.connector());
defer secondUpdate.deinit();
try secondUpdate.set(.{
.amount = 52.25,
});
try secondUpdate.whereValue(usize, "id", "=", 1);
var secondUpdateResult = try secondUpdate.update(std.testing.allocator);
secondUpdateResult.deinit();
}
// SELECT before rollback to savepoint in the transaction.
{
var queryBeforeRollbackToSavepoint = repository.MyModelRepository.Query.init(std.testing.allocator, session.connector(), .{});
try queryBeforeRollbackToSavepoint.whereValue(usize, "id", "=", 1);
defer queryBeforeRollbackToSavepoint.deinit();
// Get models.
var resultBeforeRollbackToSavepoint = try queryBeforeRollbackToSavepoint.get(std.testing.allocator);
defer resultBeforeRollbackToSavepoint.deinit();
// Check that one model has been retrieved, then check its type and values.
try std.testing.expectEqual(1, resultBeforeRollbackToSavepoint.models.len);
try std.testing.expectEqual(repository.MyModel, @TypeOf(resultBeforeRollbackToSavepoint.models[0].*));
try std.testing.expectEqual(1, resultBeforeRollbackToSavepoint.models[0].id);
try std.testing.expectEqualStrings("tempname", resultBeforeRollbackToSavepoint.models[0].name);
try std.testing.expectEqual(52.25, resultBeforeRollbackToSavepoint.models[0].amount);
}
try session.rollbackTo("my_savepoint");
// SELECT after rollback to savepoint in the transaction.
{
var queryAfterRollbackToSavepoint = repository.MyModelRepository.Query.init(std.testing.allocator, session.connector(), .{});
try queryAfterRollbackToSavepoint.whereValue(usize, "id", "=", 1);
defer queryAfterRollbackToSavepoint.deinit();
// Get models.
var resultAfterRollbackToSavepoint = try queryAfterRollbackToSavepoint.get(std.testing.allocator);
defer resultAfterRollbackToSavepoint.deinit();
// Check that one model has been retrieved, then check its type and values.
try std.testing.expectEqual(1, resultAfterRollbackToSavepoint.models.len);
try std.testing.expectEqual(repository.MyModel, @TypeOf(resultAfterRollbackToSavepoint.models[0].*));
try std.testing.expectEqual(1, resultAfterRollbackToSavepoint.models[0].id);
try std.testing.expectEqualStrings("tempname", resultAfterRollbackToSavepoint.models[0].name);
try std.testing.expectEqual(50.00, resultAfterRollbackToSavepoint.models[0].amount);
}
try session.rollbackTransaction();
// SELECT outside of the rolled back transaction.
{
var queryOutside = repository.MyModelRepository.Query.init(std.testing.allocator, session.connector(), .{});
try queryOutside.whereValue(usize, "id", "=", 1);
defer queryOutside.deinit();
// Get models.
var resultOutside = try queryOutside.get(std.testing.allocator);
defer resultOutside.deinit();
// Check that one model has been retrieved, then check its type and values.
try std.testing.expectEqual(1, resultOutside.models.len);
try std.testing.expectEqual(repository.MyModel, @TypeOf(resultOutside.models[0].*));
try std.testing.expectEqual(1, resultOutside.models[0].id);
try std.testing.expectEqualStrings("test", resultOutside.models[0].name);
try std.testing.expectEqual(50.00, resultOutside.models[0].amount);
}
}