modules/db/rowquill/tableclass.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
from rdf try import
	rdf_blank as _rq_rdf_blank_term,
	rdf_iri as _rq_rdf_iri,
	rdf_literal as _rq_rdf_literal,
	rdf_quad as _rq_rdf_quad,
	rdf_type as _rq_rdf_type,
	xsd as _rq_xsd;
from db/rowquill/sqlbuilder import
	build_limit as _rq_build_limit,
	build_order as _rq_build_order,
	build_where as _rq_build_where;
from std/net/url import escape as _rq_url_escape;
from std/string import join as _rq_join;

let Number _rq_blank_seq := 0;

function _rq_rdf_require () {
	die "db/rowquill RDF export requires optional module rdf"
		if _rq_rdf_blank_term ≡ null
		or _rq_rdf_iri ≡ null
		or _rq_rdf_literal ≡ null
		or _rq_rdf_quad ≡ null
		or _rq_rdf_type ≡ null
		or _rq_xsd ≡ null;
	return true;
}

function _rq_rdf_base ( schema ) {
	let base := schema.get_base_uri();
	die "db/rowquill RDF export requires Schema.base_uri"
		if base ≡ null or "" _ base eq "";
	return "" _ base;
}

function _rq_rdf_column_datatype ( column ) {
	let type := lc( "" _ column{type} );
	return _rq_xsd("integer")
		if type ~ /^(bigint|int|integer|smallint|tinyint)\b/;
	return _rq_xsd("decimal")
		if type ~ /^(decimal|numeric)\b/;
	return _rq_xsd("double")
		if type ~ /^(double|float|real)\b/;
	return _rq_xsd("boolean")
		if type ~ /^(bool|boolean)\b/;
	return _rq_xsd("dateTime")
		if type ~ /^(datetime|timestamp)\b/;
	return _rq_xsd("date")
		if type ~ /^date\b/;
	return _rq_xsd("time")
		if type ~ /^time\b/;
	return _rq_xsd("string");
}

function _rq_rdf_blank () {
	_rq_blank_seq++;
	return _rq_rdf_blank_term( "rowquill" _ _rq_blank_seq );
}

function _rq_rdf_result ( Array quads, PairList opts ) {
	if ( opts.exists("into") ) {
		opts{into}.add_quads(quads);
		return opts{into};
	}
	return quads;
}

trait TableClass {
	method _table_class () {
		return self.get_schema().table( self.get_table_name() );
	}

	method _column ( String col ) {
		for ( let c in self.get_column_metadata() ) {
			return c if c{name} eq col;
			return c if c.get( "accessor", "" ) eq col;
		}
		die "No such column: " _ col;
	}

	method _inflate_column ( String col, value ) {
		let c := self._column(col);
		return c{inflate}(value) if c.exists("inflate");
		return value;
	}

	method _deflate_column ( String col, value ) {
		let c := self._column(col);
		return c{deflate}(value) if c.exists("deflate");
		return value;
	}

	method _validate_column ( String col, value, raw_value ) {
		let c := self._column(col);
		if ( c.get( "required", false ) and raw_value ≡ null ) {
			die "Column " _ col _ " is required";
		}

		for ( let max_length in c.get_all("length") ) {
			if ( length raw_value > max_length ) {
				die "Column " _ col _
					" exceeds maximum length " _ max_length;
			}
		}

		for ( let pattern in c.get_all("pattern") ) {
			if ( not ( raw_value ~ pattern ) ) {
				die "Column " _ col _ " does not match pattern";
			}
		}

		for ( let validator in c.get_all("validate") ) {
			if ( not validator(value) ) {
				die "Column " _ col _ " failed validation";
			}
		}

		return true;
	}

	method _set_column ( String col, value, Boolean raw := false ) {
		let c := self._column(col);
		if ( c.get( "readonly", false ) and self{in_database} ) {
			die "Column " _ col _ " is read-only";
		}
		let raw_value := raw
			? value
			: self._deflate_column( c{name}, value );
		let validators := c.get_all("validate");
		let value_for_validation := raw and validators.length()
			? self._inflate_column( c{name}, raw_value )
			: value;
		self._validate_column(
			c{name},
			value_for_validation,
			raw_value
		);
		self{column_data}{(c{name})} := raw_value;
		self{dirty}{(c{name})} := true;
		return self;
	}

	method _get_column ( String col, Boolean raw := false ) {
		let value := self{column_data}{(col)};
		return raw ? value : self._inflate_column( col, value );
	}

	method _relationship_join_pairs ( rel ) {
		let pairs := rel{join}.to_Array();
		return pairs if rel{join} instanceof PairList;

		let ordered := [];
		for ( let column in self.get_column_metadata() ) {
			for ( let pair in pairs ) {
				ordered.push(pair) if self._column(pair.key){name} eq column{name};
			}
		}
		return ordered;
	}

	method _relationship_conditions ( rel ) {
		let conditions := {};
		for ( let pair in self._relationship_join_pairs(rel) ) {
			let local_col := self._column(pair.key){name};
			let value := self._get_column(local_col);
			return null if value ≡ null;
			conditions{(pair.value)} := value;
		}
		if ( rel.exists("where") ) {
			return { AND: [ conditions, rel{where} ] };
		}
		return conditions;
	}

	method _set_relationship ( rel, related ) {
		die "Cannot set has_many relationship " _ rel{accessor}
			if rel{type} eq "has_many";
		die "Cannot set narrowed relationship " _ rel{accessor}
			if rel.exists("where");
		for ( let pair in self._relationship_join_pairs(rel) ) {
			let local_col := self._column(pair.key){name};
			let related_col := related._column(pair.value){name};
			self._set_column(
				local_col,
				related._get_column( related_col )
			);
		}
		return self;
	}

	method _validate_database_constraints ( Array cols ) {
		for ( let col in cols ) {
			let c := self._column(col);
			let value := self{column_data}.get( c{name}, null );
			next if value ≡ null;

			if ( c.get( "unique", false ) ) {
				self._validate_unique_column(c, value);
			}
			if ( c.exists("exists_in") ) {
				self._validate_exists_in(c, value);
			}
		}
		return true;
	}

	method _validate_required_columns_for_write () {
		for ( let c in self.get_column_metadata() ) {
			let col := c{name};
			if ( c.get( "required", false ) ) {
				if (
					not self{column_data}.exists(col)
					or self{column_data}{(col)} ≡ null
				) {
					die "Column " _ col _ " is required";
				}
			}
		}
		return true;
	}

	method _has_one ( rel ) {
		let conditions := self._relationship_conditions(rel);
		return null if conditions ≡ null;
		let rows := self.get_schema()
			.table(rel{table})
			._search_run(conditions, {});
		return rows[0] if rows.length() > 0;
		return null;
	}

	method _has_many ( rel ) {
		let conditions := self._relationship_conditions(rel);
		return [] if conditions ≡ null;
		return self.get_schema()
			.table(rel{table})
			._search_run(conditions, {});
	}

	method _apply_defaults () {
		for ( let c in self.get_column_metadata() ) {
			next if not c.exists("default");
			next if self{column_data}.exists(c{name});
			let value := c{default} instanceof Function
				? c{default}(self)
				: c{default};
			self._set_column( c{name}, value );
		}
		return self;
	}

	method _run_hooks ( String name ) {
		for ( let hook in self.get_hook_metadata(){(name)} ) {
			hook(self);
		}
		return self;
	}

	method _rdf_table_iri () {
		return _rq_rdf_iri(
			_rq_rdf_base(self.get_schema()) _
			_rq_url_escape(self.get_table_name())
		);
	}

	method _rdf_column_iri ( String col ) {
		return _rq_rdf_iri(
			_rq_rdf_base(self.get_schema()) _
			_rq_url_escape(self.get_table_name()) _
			"#" _
			_rq_url_escape(col)
		);
	}

	method _rdf_ref_iri ( rel ) {
		let cols := self._relationship_join_pairs(rel).map( function ( pair ) {
			return _rq_url_escape(self._column(pair.key){name});
		} );
		return _rq_rdf_iri(
			_rq_rdf_base(self.get_schema()) _
			_rq_url_escape(self.get_table_name()) _
			"#ref-" _
			_rq_join( ";", cols )
		);
	}

	method _rdf_row_node () {
		let pkey_names := self._table_class()._primary_key_names();
		return _rq_rdf_blank() if pkey_names.length() = 0;

		let parts := [];
		for ( let pkey_name in pkey_names ) {
			die "Missing primary key value for " _ pkey_name
				if not self{column_data}.exists(pkey_name)
				or self{column_data}{(pkey_name)} ≡ null;
			parts.push(
				_rq_url_escape(pkey_name) _ "=" _
				_rq_url_escape("" _ self{column_data}{(pkey_name)})
			);
		}

		return _rq_rdf_iri(
			_rq_rdf_base(self.get_schema()) _
			_rq_url_escape(self.get_table_name()) _
			"/" _
			_rq_join( ";", parts )
		);
	}

	method _rdf_literal_for_column ( column ) {
		return _rq_rdf_literal(
			"" _ self{column_data}{(column{name})},
			"",
			_rq_rdf_column_datatype(column),
		);
	}

	method _rdf_reference_quads ( subject ) {
		let quads := [];
		for ( let rel in self.get_relationship_metadata() ) {
			next if rel{type} ne "has_one";

			let complete := true;
			for ( let pair in self._relationship_join_pairs(rel) ) {
				let local_col := self._column(pair.key){name};
				if (
					not self{column_data}.exists(local_col)
					or self._get_column(local_col) == null
				) {
					complete := false;
				}
			}
			next if not complete;

			let related := self._has_one(rel);
			die "Cannot map relationship " _ rel{accessor} _
				" to RDF: target row not found"
				if related ≡ null;
			die "Cannot map relationship " _ rel{accessor} _
				" to RDF: target table has no primary key"
				if related._table_class()._primary_key_names().length() = 0;

			quads.push(_rq_rdf_quad(
				subject,
				self._rdf_ref_iri(rel),
				related._rdf_row_node(),
			));
		}
		return quads;
	}

	method _rdf_extra_quads () {
		let quads := [];
		for ( let hook in self.get_rdf_hook_metadata() ) {
			let extra := hook(self);
			die "on_rdf callback must return an Array"
				if not ( extra instanceof Array );
			for ( let quad in extra ) {
				quads.push(quad);
			}
		}
		return quads;
	}

	method as_rdf ( ... PairList opts ) {
		_rq_rdf_require();
		_rq_rdf_base(self.get_schema());

		let subject := self._rdf_row_node();
		let quads := [
			_rq_rdf_quad( subject, _rq_rdf_type(), self._rdf_table_iri() ),
		];

		for ( let column in self.get_column_metadata() ) {
			next if not self{column_data}.exists(column{name});
			next if self._get_column(column{name}) == null;
			quads.push(_rq_rdf_quad(
				subject,
				self._rdf_column_iri(column{name}),
				self._rdf_literal_for_column(column),
			));
		}

		for ( let quad in self._rdf_reference_quads(subject) ) {
			quads.push(quad);
		}
		for ( let quad in self._rdf_extra_quads() ) {
			quads.push(quad);
		}

		return _rq_rdf_result( quads, opts );
	}

	method _validate_unique_column ( column, value ) {
		from std/string import join, sprint;
		let binds := [ value ];
		let where := [
			self._table_class()._sql_identifier(column{name}) _ "=?",
		];
		if ( self{in_database} ) {
			for ( let pkey_name in self._table_class()._primary_key_names() ) {
				die "Missing primary key value for " _ pkey_name
					if not self{column_data}.exists(pkey_name);
				where.push(
					self._table_class()._sql_identifier(pkey_name) _ "!=?"
				);
				binds.push( self{column_data}{(pkey_name)} );
			}
		}
		let sql := sprint(
			"SELECT 1 AS found FROM %s WHERE %s",
			self._table_class()._sql_table(),
			join( " AND ", where ),
		);
		let sth := self.get_schema().get_dbh().prepare(sql);
		sth.execute( ... binds );
		die "Column " _ column{name} _ " must be unique"
			if sth.next_typed_dict();
		return true;
	}

	method _validate_exists_in ( column, value ) {
		from std/string import split, sprint;
		let spec := column{exists_in};
		let parts := split( spec, ".", 2 );
		die "Column " _ column{name} _
			" exists_in must be table.column"
			if parts.length() ≢ 2
			or not ( parts[0] ~ /^[A-Za-z_][A-Za-z0-9_]*$/ )
			or not ( parts[1] ~ /^[A-Za-z_][A-Za-z0-9_]*$/ );
		let tab := parts[0];
		let col := parts[1];
		let sql := sprint(
			"SELECT 1 AS found FROM %s WHERE %s=?",
			self._table_class()._sql_identifier(tab),
			self._table_class()._sql_identifier(col),
		);
		let sth := self.get_schema().get_dbh().prepare(sql);
		sth.execute(value);
		die "Column " _ column{name} _ " refers to missing " _
			column{exists_in} if not sth.next_typed_dict();
		return true;
	}

	method insert () {
		from std/string import join, sprint;

		die "Cannot insert row already in database"
			if self{in_database};
		self._apply_defaults();
		self._validate_required_columns_for_write();

		let cols := self.get_column_metadata().map( function ( c ) {
			return c{name};
		} ).grep( function ( col ) {
			return self{column_data}.exists(col);
		} );
		die "No column values to insert into " _ self.get_table_name()
			if cols.length = 0;
		self._validate_database_constraints(cols);
		self._run_hooks("before_insert");

		let placeholders := cols.map( function ( col ) {
			return "?";
		} );
		let sql := sprint(
			"INSERT INTO %s (%s) VALUES (%s)",
			self._table_class()._sql_table(),
			join( ", ", cols.map( function ( col ) {
				return self._table_class()._sql_identifier(col);
			} ) ),
			join( ", ", placeholders ),
		);
		let values := cols.map( function ( col ) {
			return self{column_data}{(col)};
		} );
		self.get_schema().get_dbh().prepare(sql).execute( ... values );
		self{dirty} := {};
		self{in_database} := true;
		self._run_hooks("after_insert");
		return self;
	}

	method update () {
		from std/string import join, sprint;

		die "Cannot update row not in database"
			if not self{in_database};
		self._validate_required_columns_for_write();

		let pkey_names := self._table_class()._primary_key_names();
		die "Cannot update table without primary key: " _
			self.get_table_name()
			if pkey_names.length = 0;

		let pkey_lookup := {};
		for ( let pkey_name in pkey_names ) {
			pkey_lookup{(pkey_name)} := true;
		}

		let dirty_pkeys := pkey_names.grep( function ( col ) {
			return self{dirty}.get( col, false );
		} );
		die "Cannot update dirty primary key fields: " _
			join( ", ", dirty_pkeys )
			if dirty_pkeys.length;

		let cols := self.get_column_metadata().map( function ( c ) {
			return c{name};
		} ).grep( function ( col ) {
			return self{dirty}.get( col, false )
				and not pkey_lookup.exists(col);
		} );
		return self if cols.length = 0;
		self._validate_database_constraints(cols);
		self._run_hooks("before_update");

		let assignments := cols.map( function ( col ) {
			return self._table_class()._sql_identifier(col) _ "=?";
		} );
		let where := pkey_names.map( function ( col ) {
			return self._table_class()._sql_identifier(col) _ "=?";
		} );
		let sql := sprint(
			"UPDATE %s SET %s WHERE %s",
			self._table_class()._sql_table(),
			join( ", ", assignments ),
			join( " AND ", where ),
		);

		let values := cols.map( function ( col ) {
			return self{column_data}{(col)};
		} );
		for ( let pkey_name in pkey_names ) {
			die "Missing primary key value for " _ pkey_name
				if not self{column_data}.exists(pkey_name);
			values.push( self{column_data}{(pkey_name)} );
		}

		self.get_schema().get_dbh().prepare(sql).execute( ... values );
		self{dirty} := {};
		self._run_hooks("after_update");
		return self;
	}

	method delete () {
		from std/string import join, sprint;

		die "Cannot delete row not in database"
			if not self{in_database};

		let pkey_names := self._table_class()._primary_key_names();
		die "Cannot delete from table without primary key: " _
			self.get_table_name() if pkey_names.length = 0;
		self._run_hooks("before_delete");

		let where := pkey_names.map( function ( col ) {
			return self._table_class()._sql_identifier(col) _ "=?";
		} );
		let sql := sprint(
			"DELETE FROM %s WHERE %s",
			self._table_class()._sql_table(),
			join( " AND ", where ),
		);
		let values := [];
		for ( let pkey_name in pkey_names ) {
			die "Missing primary key value for " _ pkey_name
				if not self{column_data}.exists(pkey_name);
			values.push( self{column_data}{(pkey_name)} );
		}

		self.get_schema().get_dbh().prepare(sql).execute( ... values );
		self{dirty} := {};
		self{in_database} := false;
		self._run_hooks("after_delete");
		return self;
	}

	method _rq_find ( pkey ) {
		const pkey_names := self._table_class()._primary_key_names();
		let pkey_values := [];
		if ( pkey instanceof Array ) {
			let i := 0;
			while ( i < pkey_names.length() ) {
				pkey_values.push(
					self._deflate_column( pkey_names[i], pkey[i] )
				);
				++i;
			}
		}
		else if ( pkey instanceof Dict or pkey instanceof PairList ) {
			for ( let pkey_name in pkey_names ) {
				pkey_values.push(
					self._deflate_column(
						pkey_name,
						pkey{(pkey_name)}
					)
				);
			}
		}
		else if ( pkey_names.length = 1 ) {
			pkey_values := [
				self._deflate_column( pkey_names[0], pkey )
			];
		}
		else {
			die "Expected Array or Dict of primary key values";
		}

		from std/string import sprint, join;
		let sql := sprint(
			"SELECT * FROM %s WHERE %s",
			self._table_class()._sql_table(),
			join( " AND ", pkey_names.map( function ( col ) {
				return self._table_class()._sql_identifier(col) _ "=?";
			} ) ),
		);
		let sth := self.get_schema().get_dbh().prepare(sql);
		sth.execute( ... pkey_values );
		let row := sth.next_typed_dict();
		if ( row ) {
			return self._table_class()._from_database(row);
		}
		return null;
	}

	method _rq_search_opts ( PairList conditions ) {
		let opts := conditions.get( "opts", {} );
		let clean := conditions.copy;
		clean.remove("opts");
		return { conditions: clean, opts: opts };
	}

	method _rq_build_where ( conditions ) {
		let table_class := self._table_class();
		return _rq_build_where(
			conditions,
			function ( String col ) {
				return table_class._sql_column(col);
			},
			function ( String col, value ) {
				return self._deflate_column( col, value );
			},
		);
	}

	method _rq_build_order ( opts ) {
		let table_class := self._table_class();
		return _rq_build_order(
			opts,
			function ( String col ) {
				return table_class._sql_column(col);
			},
		);
	}

	method _rq_search_run ( conditions, opts ) {
		from std/string import sprint;

		let where := self._rq_build_where(conditions);
		let limit := _rq_build_limit(opts);
		let binds := where{binds};
		for ( let value in limit{binds} ) {
			binds.push(value);
		}
		let sql := sprint(
			"SELECT * FROM %s WHERE %s",
			self._table_class()._sql_table(),
			where{sql},
		) _ self._rq_build_order(opts) _ limit{sql};
		let sth := self.get_schema().get_dbh().prepare(sql);
		sth.execute( ... binds );

		let rows := [];
		for ( let row in sth.all_typed_dict() ) {
			rows.push( self._table_class()._from_database(row) );
		}
		return rows;
	}

	method _rq_search ( PairList conditions ) {
		let parts := self._rq_search_opts(conditions);
		return self._rq_search_run( parts{conditions}, parts{opts} );
	}

	method _rq_all ( PairList args ) {
		return self._rq_search_run( {}, args.get( "opts", {} ) );
	}

	method _rq_first ( PairList conditions ) {
		let parts := self._rq_search_opts(conditions);
		let opts := parts{opts}.copy;
		opts{limit} := 1;
		let rows := self._rq_search_run( parts{conditions}, opts );
		return rows.length() ? rows[0] : null;
	}

	method _rq_count ( PairList conditions ) {
		let parts := self._rq_search_opts(conditions);
		return self._rq_count_run(parts{conditions});
	}

	method _rq_exists ( PairList conditions ) {
		return self._rq_count(conditions) > 0;
	}

	method _rq_count_run ( conditions ) {
		from std/string import sprint;
		let where := self._rq_build_where(conditions);
		let sql := sprint(
			"SELECT COUNT(*) AS rowquill_count FROM %s WHERE %s",
			self._table_class()._sql_table(),
			where{sql},
		);
		let sth := self.get_schema().get_dbh().prepare(sql);
		sth.execute( ... where{binds} );
		return sth.next_typed_dict(){rowquill_count};
	}

	method _rq_find_or_create ( PairList opts ) {
		let rows := self._rq_search_run( opts{find}, { limit: 1 } );
		return rows[0] if rows.length();
		let data := {};
		for ( let pair in opts{find}.to_Array() ) {
			next if pair.key eq "AND" or pair.key eq "OR" or pair.key eq "NOT";
			next if pair.value instanceof Array;
			data{(pair.key)} := pair.value;
		}
		for ( let pair in opts.get( "create", {} ).to_Array() ) {
			data{(pair.key)} := pair.value;
		}
		let row := self._table_class()._create_from(data);
		row.insert();
		return row;
	}

	method _rq_create_or_update ( PairList opts ) {
		let rows := self._rq_search_run( opts{find}, { limit: 1 } );
		let row := rows.length()
			? rows[0]
			: self._table_class()._create_from(opts{find});
		for ( let pair in opts.get( "set", {} ).to_Array() ) {
			row._set_column( pair.key, pair.value );
		}
		row.insert_or_update();
		return row;
	}

	method is_dirty () {
		return self{dirty}.to_Array().length() > 0;
	}

	method dirty_fields () {
		return self{dirty}.to_Array().grep(
			fn pair → pair.value
		).map(
			fn pair → pair.key
		);
	}

	method mark_clean () {
		self{dirty} := {};
		return self;
	}

	method reload () {
		die "Cannot reload row not in database"
			if not self{in_database};
		let fresh := self._table_class().find( self._primary_key_value() );
		die "Row no longer exists in database" if fresh ≡ null;
		self{column_data} := fresh{column_data}.copy;
		self{dirty} := {};
		self{in_database} := true;
		return self;
	}

	method _primary_key_value () {
		let pkeys := self._table_class()._primary_key_names();
		die "Cannot use row without primary key"
			if pkeys.length = 0;
		if ( pkeys.length = 1 ) {
			die "Missing primary key value for " _ pkeys[0]
				if not self{column_data}.exists(pkeys[0]);
			return self._get_column(pkeys[0]);
		}
		let values := [];
		for ( let pkey in pkeys ) {
			die "Missing primary key value for " _ pkey
				if not self{column_data}.exists(pkey);
			values.push( self._get_column(pkey) );
		}
		return values;
	}

	method insert_or_update () {
		try { self.insert() } catch (e) { self.update() };
	}

	method _rq_all_as_rdf ( PairList opts ) {
		let quads := [];
		let search_opts := opts.get( "opts", {} );
		for ( let row in self._rq_search_run( {}, search_opts ) ) {
			let export_row := row;
			if ( row._table_class()._primary_key_names().length() > 0 ) {
				let fresh := row._table_class().find(row._primary_key_value());
				export_row := fresh if fresh ≢ null;
			}
			for ( let quad in export_row.as_rdf() ) {
				quads.push(quad);
			}
		}
		return _rq_rdf_result( quads, opts );
	}
}

class ClassMaker {
	let table;
	let schema;
	let _cols := [];
	let _pkey := [];
	let _rels := [];
	let _helpers := [];
	let _traits := [];
	let _rdf_hooks := [];
	let _hooks := {
		before_insert: [],
		after_insert:  [],
		before_update: [],
		after_update:  [],
		before_delete: [],
		after_delete:  [],
	};

	method add_column ( String colname, String type, ... PairList pl ) {
		const opts := pl ? pl.copy : {{}};
		opts{name} := colname;
		opts{type} := type;
		_cols.push( opts );
		_pkey.push( opts ) if opts{primary};
	}

	method add_helper ( String methodname, Function cb, ... PairList pl ) {
		const opts := pl ? pl.copy : {{}};
		opts{name} := methodname;
		opts{callback} := cb;
		_helpers.push( opts );
	}

	method add_static ( String methodname, Function cb, ... PairList pl ) {
		const opts := pl ? pl.copy : {{}};
		opts{name} := methodname;
		opts{callback} := cb;
		opts{is_static} := true;
		_helpers.push( opts );
	}

	method add_trait ( t ) {
		_traits.push( t );
	}

	method before_insert ( Function cb ) { _hooks{before_insert}.push(cb); }
	method after_insert  ( Function cb ) { _hooks{after_insert}.push(cb);  }
	method before_update ( Function cb ) { _hooks{before_update}.push(cb); }
	method after_update  ( Function cb ) { _hooks{after_update}.push(cb);  }
	method before_delete ( Function cb ) { _hooks{before_delete}.push(cb); }
	method after_delete  ( Function cb ) { _hooks{after_delete}.push(cb);  }
	method on_rdf ( Function cb ) { _rdf_hooks.push(cb); }

	method has_one ( ... PairList pl ) {
		const opts := pl.copy;
		self._validate_relationship_opts(opts);
		opts{type} := "has_one";
		_rels.push(opts);
	}

	method has_many ( ... PairList pl ) {
		const opts := pl.copy;
		self._validate_relationship_opts(opts);
		opts{type} := "has_many";
		_rels.push(opts);
	}

	method _validate_relationship_opts ( opts ) {
		let join := opts{join};
		die "Relationship join must be a Dict or PairList"
			if not ( join instanceof Dict or join instanceof PairList );
		die "Relationship where must be a Dict or PairList"
			if opts.exists("where")
			and not (
				opts{where} instanceof Dict
				or opts{where} instanceof PairList
			);
		return true;
	}

	method _make_accessors () {
		from std/string import join;
		return join( "\n", self{_cols}.map( function (c) {
			const colname := c{name};
			const accessorname := c{accessor} ?: c{name};
			let code := ```
				method ${accessorname} ( ... PairList opts ) {
					if ( opts.exists("set") ) {
						self._set_column(
							"${colname}",
							opts{set},
							opts.get( "raw", false )
						);
					}
					return self._get_column(
						"${colname}",
						opts.get( "raw", false )
					);
				}
			```;
			if ( accessorname ne colname ) {
				code _= ```
					method ${colname} ( ... PairList opts ) {
						return self.${accessorname}( ... opts );
					}
				```;
			}
			return code;
		} ) );
	}

	method _make_relationship_accessors () {
		from std/string import join;
		let i := -1;
		return join( "\n", self{_rels}.map( function (r) {
			i++;
			const accessorname := r{accessor};
			if ( r{type} eq "has_one" ) {
				return ```
				method ${accessorname} ( ... PairList opts ) {
					return self._set_relationship(
						self.get_relationship_metadata()[${i}],
						opts{set}
					) if opts.exists("set");
					return self._has_one(
						self.get_relationship_metadata()[${i}]
					);
				}
				```;
			}
			return ```
			method ${accessorname} ( ... PairList opts ) {
				return self._set_relationship(
					self.get_relationship_metadata()[${i}],
					opts{set}
				) if opts.exists("set");
				return self._has_many(
					self.get_relationship_metadata()[${i}]
				);
			}
			```;
		} ) );
	}

	method _make_helpers () {
		from std/string import join;
		let i := -1;
		return join( "\n", self{_helpers}.map( function (h) {
			i++;
			let metadata := h{is_static}
				? "self._helper_metadata()"
				: "self.get_helper_metadata()";
			return ```
				${ h{is_static} ? "static method" : "method" } ${ h{name} } ( ... Array a, PairList p ) {
					return ${metadata}[${i}]{callback}( self, ...a, ...p );
				}
			```
		} ) );
	}

	method make_class () {
		const CODE := do {
			from std/string import camel, join;
			let class_name := camel( self{table} );
			class_name[0] := "TABLE_" _ uc class_name[0];

			let i := 0;
			let trait_defs := "";
			let trait_slug := "";
			for ( const t in self{_traits} ) {
				trait_defs _= `; const RowquillTrait${i} := TRAITS[${i}]`;
				trait_slug _= `, RowquillTrait${i}`;
				++i;
			}

			```
			let _cached_pkeys${trait_defs};

			class ${class_name} with TableClass${trait_slug} {
				let column_data := {};
				let dirty := {};
				let in_database := false;

				method get_schema () {
					return SCHEMA;
				}

				method get_table_name () {
					return TABLE_NAME;
				}

				method get_column_metadata () {
					return COLUMNS;
				}

				method get_relationship_metadata () {
					return RELATIONSHIPS;
				}

				method get_helper_metadata () {
					return HELPERS;
				}

				method get_hook_metadata () {
					return HOOKS;
				}

				method get_rdf_hook_metadata () {
					return RDF_HOOKS;
				}

				static method _schema () {
					return SCHEMA;
				}

				static method _table_name () {
					return TABLE_NAME;
				}

				static method _column_metadata () {
					return COLUMNS;
				}

				static method _helper_metadata () {
					return HELPERS;
				}

				static method _sql_identifier ( String name ) {
					die "Invalid SQL identifier: " _ name
						if not ( name ~ /^[A-Za-z_][A-Za-z0-9_]*$/ );
					return "\"" _ name _ "\"";
				}

				static method _column_def ( String col ) {
					for ( let c in self._column_metadata() ) {
						return c if c{name} eq col;
						return c if c.get( "accessor", "" ) eq col;
					}
					die "No such column: " _ col;
				}

				static method _sql_column ( String col ) {
					return self._sql_identifier( self._column_def(col){name} );
				}

				static method _sql_table () {
					return self._sql_identifier( self._table_name() );
				}

				static method _primary_key_names () {
					_cached_pkeys ?:= self._column_metadata().grep(
						→ ^^.get( "primary", false )
					);
					return _cached_pkeys.map( → ^^{name} );
				}

				static method create ( ... PairList opts ) {
					return self._create_from(opts);
				}

				static method _create_from ( data ) {
					let i := new self();
					for ( data.to_Array() ) {
						i._set_column( ^^.key, ^^.value );
					}
					i._apply_defaults();
					return i;
				}

				static method _from_database ( Dict row ) {
					let i := new self();
					for ( let pair in row.to_Array() ) {
						i{column_data}{(pair.key)} := pair.value;
					}
					i{dirty} := {};
					i{in_database} := true;
					return i;
				}

				static method find ( pkey ) {
					return ( new self() )._rq_find(pkey);
				}

				static method _search_run ( conditions, opts ) {
					return ( new self() )._rq_search_run( conditions, opts );
				}

				static method search ( ... PairList conditions ) {
					return ( new self() )._rq_search(conditions);
				}

				static method all ( ... PairList args ) {
					return ( new self() )._rq_all(args);
				}

				static method all_as_rdf ( ... PairList args ) {
					return ( new self() )._rq_all_as_rdf(args);
				}

				static method first ( ... PairList conditions ) {
					return ( new self() )._rq_first(conditions);
				}

				static method count ( ... PairList conditions ) {
					return ( new self() )._rq_count(conditions);
				}

				static method exists ( ... PairList conditions ) {
					return ( new self() )._rq_exists(conditions);
				}

				static method _count_run ( conditions ) {
					return ( new self() )._rq_count_run(conditions);
				}

				static method find_or_create ( ... PairList opts ) {
					return ( new self() )._rq_find_or_create(opts);
				}

				static method create_or_update ( ... PairList opts ) {
					return ( new self() )._rq_create_or_update(opts);
				}

				${self._make_accessors()}
				${self._make_relationship_accessors()}
				${self._make_helpers()}
			}
			${class_name};
			```
		};

		from std/eval import eval;
		const SCHEMA     := self{schema};
		const TABLE_NAME := self{table};
		const COLUMNS    := self{_cols};
		const RELATIONSHIPS := self{_rels};
		const HELPERS    := self{_helpers};
		const HOOKS      := self{_hooks};
		const TRAITS     := self{_traits};
		const RDF_HOOKS  := self{_rdf_hooks};
		return eval( CODE );
	}
}