modules/db/rowquill.zzm

rowquill-0.0.2 source code

Package

Name
rowquill
Version
0.0.2
Uploaded
2026-06-15 20:42:01
Repository
https://github.com/tobyink/zuzu-rowquill
Dependencies
Metadata
zuzu-distribution.json
Archive
Download .tar.gz
=encoding utf8

=head1 NAME

db/rowquill - Small row-object ORM for ZuzuScript.

=head1 SYNOPSIS

  from db/rowquill import Schema;
  from std/db import DB;

  let schema := new Schema( dbh: DB.temp() );

  schema.add_table( "employee", function ( tab ) {
      tab.add_column( "id", "int", primary: true );
      tab.add_column( "name", "varchar", required: true );
  } );

  let bob := schema.table("employee").create( id: 1, name: "Bob" );
  bob.insert();

  say( schema.table("employee").find(1).name() );

=head1 DESCRIPTION

C<db/rowquill> is a small row-object ORM for ZuzuScript. A C<Schema>
wraps a C<std/db> database handle and builds one generated class per
table. Generated row classes support C<create>, C<find>, C<search>,
C<insert>, C<update>, C<delete>, column accessors, inflate/deflate
callbacks, validation, and simple C<has_one> and C<has_many>
relationships. Table builders can also add custom row helper methods
and static table-class helper methods.

=head2 Schema Helpers

C<Schema.has_table> checks whether a table has been registered, and
C<Schema.table_names> returns the registered table names. C<Schema.find>,
C<Schema.search>, C<Schema.create>, and C<Schema.insert> are convenience
wrappers around the generated table class for cases where the table name is
dynamic.

=head2 Searching

C<search> accepts named column conditions. Operator conditions are arrays
containing the SQL-ish operator and value. Supported operators include
C<LIKE>, C<NOT LIKE>, C<ILIKE>, C<IN>, C<NOT IN>, and C<BETWEEN>.

  let Employee := schema.table("employee");
  Employee.search(
      name: [ "ILIKE", "%rob%" ],
      id:   [ "IN", [ 1, 2, 3 ] ],
      opts: { order_by: [ [ "name", "ASC" ] ], limit: 20 },
  );

Condition groups may be nested with C<AND>, C<OR>, and C<NOT>. Query
options live under C<opts>. Convenience methods C<all>, C<first>,
C<count>, C<exists>, C<find_or_create>, and C<create_or_update> are also
available.

Relationship joins are declared as mappings from columns on the current
row to columns on the related table. Multiple mappings are combined
with AND. Relationships may also include a C<where> condition using the
same condition style as C<search>.

  tab.has_many(
      accessor: "employees",
      table:    "employee",
      join:     { id: "dept", company: "company" },
	      where:    { is_deleted: false },
	  );

A non-narrowed C<has_one> relationship may be assigned with
C<relationship(set: row)>; this copies every join column from the related
row. C<has_many> relationships and relationships with C<where> filters
cannot be assigned this way.

=head2 RDF Export

When the optional C<rdf> distribution is available, C<as_rdf> exports a row
as RDF quads using the W3C Direct Mapping shape. C<all_as_rdf> on a table
class exports every row in that table, and C<Schema.all_as_rdf> exports every
registered table. Passing C<into: store> adds the quads to an RDF store and
returns the store.

  let schema := new Schema(
      dbh: DB.temp(),
      base_uri: "http://foo.example/DB/",
  );

  let quads := schema.table("employee").find(42).as_rdf();
  schema.table("employee").all_as_rdf( into: store );
  schema.all_as_rdf( into: store );

RDF export uses Rowquill metadata as the schema source of truth. Columns
marked with C<primary: true> identify rows, non-null column values become
literal triples, and C<has_one> relationships generate C<#ref-> predicates
when all local join columns are non-null. If C<rdf> is unavailable or the
schema has no C<base_uri>, RDF export throws a runtime error.

C<on_rdf> registers a table-level callback for additional RDF quads. The
callback receives the row and must return an array of quads to append to the
default generated quads.

  tab.on_rdf( function ( employee ) {
      return [
          rdf_quad(
              employee._rdf_row_node(),
              rdf_iri("http://example.com/audit"),
              rdf_literal("custom"),
          ),
      ];
  } );

=head2 Helper Methods

C<add_helper> adds an instance method to generated row objects. The
callback receives the row object as its first argument, followed by any
arguments passed to the generated method.

  tab.add_helper( "is_accountant", function ( employee ) {
      return employee.department().name() eq "Accounts";
	  } );

C<add_static> adds a static method to the generated table class. The
callback receives the table class as its first argument.

  tab.add_static( "named", function ( Employee, String name ) {
      return Employee.search( name: name );
	  } );

C<add_trait> composes a custom trait into the generated row class. Traits
are useful when several tables should share row-object behaviour.

  trait HasSlug {
      method slug () {
          return lc self.name();
      }
  }

  tab.add_trait( HasSlug );

=head2 Column Options

Column options include C<primary>, C<accessor>, C<inflate>, C<deflate>,
C<length>, C<pattern>, C<validate>, C<required>, C<default>,
C<readonly>, C<unique>, and C<exists_in>. Multiple C<length>,
C<pattern>, and C<validate> options are honoured.

C<default> may be a value or callback. C<readonly> prevents a column from
being changed after insertion. C<unique> checks for an existing non-null
value before writes. C<exists_in> checks a non-null value against another
table and column using a C<table.column> string.

=head2 Hooks

Table builders can register lifecycle hooks. Each callback receives the row
object. Supported hooks are C<before_insert>, C<after_insert>,
C<before_update>, C<after_update>, C<before_delete>, and C<after_delete>.

=head2 Transactions

C<Schema.transaction> runs a callback inside a transaction and returns the
callback result. Exceptions roll the transaction back and are rethrown.
Nested transactions use savepoints.

=head1 EXPORTS

=head2 C<Schema>

The main ORM class.

=head2 C<ClassMaker>

The table builder class passed to C<Schema.add_table>. This is exported
mostly for subclassing and advanced integration.

=head1 COPYRIGHT AND LICENCE

B<< db/rowquill >> is copyright Toby Inkster.

It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.

=cut

from db/rowquill/tableclass import ClassMaker as _RowquillClassMaker;
from rdf try import rdf_iri as _RowquillRdfProbe;

function _rowquill_rdf_require () {
	die "db/rowquill RDF export requires optional module rdf"
		if _RowquillRdfProbe ≡ null;
	return true;
}

function _rowquill_rdf_require_base ( schema ) {
	die "db/rowquill RDF export requires Schema.base_uri"
		if schema.get_base_uri() ≡ null
		or "" _ schema.get_base_uri() eq "";
	return true;
}

class ClassMaker extends _RowquillClassMaker;

class Schema {
	let _tables := {};
	let dbh with get, set;
	let base_uri with get, set := null;
	let _transaction_depth := 0;
	let _savepoint_seq := 0;

	method add_table ( String tablename, Function cb, ... PairList opts ) {
		const cm := new ClassMaker( table: tablename, schema: self );
		cb( cm );
		const k := cm.make_class();
		self{_tables}{(tablename)} := k;
		return k;
	}

	method table ( String tablename ) {
		die `No such table: ${tablename}` if not _tables.exists(tablename);
		return self{_tables}{(tablename)};
	}

	method has_table ( String tablename ) {
		return _tables.exists(tablename);
	}

	method table_names () {
		return _tables.to_Array().map( fn pair → pair.key );
	}

	method find ( String tablename, pkey ) {
		return self.table(tablename).find(pkey);
	}

	method search ( String tablename, ... PairList conditions ) {
		return self.table(tablename).search( ...conditions );
	}

	method create ( String tablename, ... PairList opts ) {
		return self.table(tablename).create( ...opts );
	}

	method insert ( String tablename, ... PairList opts ) {
		let row := self.create( tablename, ...opts );
		row.insert();
		return row;
	}

	method all_as_rdf ( ... PairList opts ) {
		_rowquill_rdf_require();
		_rowquill_rdf_require_base(self);

		let quads := [];
		for ( let tablename in self.table_names() ) {
			for ( let quad in self.table(tablename).all_as_rdf() ) {
				quads.push(quad);
			}
		}
		if ( opts.exists("into") ) {
			opts{into}.add_quads(quads);
			return opts{into};
		}
		return quads;
	}

	method _exec_sql ( String sql ) {
		self{dbh}.prepare(sql).execute();
	}

	method transaction ( Function cb ) {
		if ( self{_transaction_depth} = 0 ) {
			self{dbh}.begin();
			self{_transaction_depth}++;
			try {
				let result := cb(self);
				self{_transaction_depth}--;
				self{dbh}.commit();
				return result;
			}
			catch (e) {
				self{_transaction_depth}--;
				self{dbh}.rollback();
				throw e;
			}
		}

		self{_savepoint_seq}++;
		let sp := "rowquill_sp_" _ self{_savepoint_seq};
		self._exec_sql( "SAVEPOINT " _ sp );
		self{_transaction_depth}++;
		try {
			let result := cb(self);
			self{_transaction_depth}--;
			self._exec_sql( "RELEASE SAVEPOINT " _ sp );
			return result;
		}
		catch (e) {
			self{_transaction_depth}--;
			self._exec_sql( "ROLLBACK TO SAVEPOINT " _ sp );
			self._exec_sql( "RELEASE SAVEPOINT " _ sp );
			throw e;
		}
	}
}