Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

OOSQL is now Esquel, still a work in progress.

  • Loading branch information...
commit 04f52f6955a0cbb2041bcbbb26906b448417788f 1 parent c4808f9
novus authored
View
41 README
@@ -1,21 +1,38 @@
-OOSQL
+Esquel -- An Object-Oriented SQL Library for Perl 6
-------
-This is a Perl 6 Role, which allows you to build classes which represent
-tables in an SQL database. It provides several methods that abstract out
-the SQL syntax, so instead of doing:
+NOTE: This is a work in progress, and doesn't currently work at all.
+ When it's done, I'll be moving onto Oharem, which is referred to
+ a few times in here, but hasn't even been started yet.
+ Oh, and until Rakudo "nom" is fixed, and MiniDBI is ported to it,
+ the Esquel::DBI library will only work on the "ng" branch of Rakudo.
- my $result = $db.sql("SELECT name,job FROM mytable WHERE id = $id;");
- if ($result) {
- my @rows = $result.rows();
- }
+A simple way to generate SQL statements using native Perl 6 calls.
+Inspired by SQL::Abstract from Perl 5.
-You can do:
+For example (using the MiniDBI interface):
- my @rows = $mytable.where(:$id).select(:name,:job);
+ my $sql = Esquel.new;
+ my ($stmt, @bind) = $sql.bind.from($table).where(:$id).select('name', 'job');
+ ## SELECT name, job FROM $table WHERE id = $id;
+ my $sth = $dbh.prepare($stmt);
+ $sth.execute(|@bind);
+ my $result = $sth.fetchrow_hashref;
+ say "{$result<name>} is a {$result<job>}";
-NOTE: This is completely unfinished and still in early development stage.
- Don't try to use this, it doesn't actually work yet.
+For an even simpler interface (but assuming a DBI-compatible library)
+you can use the Esquel::DBI wrapper, which turns the above into:
+
+ ## Pass it the DBH object returned from a connect() call.
+ my $db = Esquel::DBI.new($dbh);
+ ## This assumes 'bind' by default, to override, use 'nobind'.
+ my $result = $db.where(:$id).select('name', 'job').row;
+ say "{$result<name>} is a {$result<job>}";
+
+TODO: After getting Esquel::DBI up and running, I intend to use it as the
+ foundation for a new library called Oharem. For details on that, see
+ the doc/oharem.txt file.
Author: Timothy Totten
License: Artistic License 2.0
+
View
66 doc/oharem.txt
@@ -0,0 +1,66 @@
+Oharem -- A future DB ORM project using Esquel::DBI as its foundation.
+
+The best way to describe what it will look like, is to show an example.
+
+ class User does Oharem::Row {
+
+ use JSON::Tiny;
+
+ ## Rules:
+ ## primary -- Use this in the WHERE clause to update records.
+ ## auto -- This will be generated by the DB on first creation.
+ ## load -- Call self.load-$name($field-from-db) to populate $!name
+ ## save -- Call self.save-$name() to save the 'name' field to the DB.
+ has %!DB-rules = {
+ id => { :primary, :auto },
+ details => { :load, :store },
+ }
+
+ ## Any attribute that doesn't start with DB will be considered a
+ ## database field. So a field of 'id' will be $.id, and so forth.
+ ## The Primary field should be readonly.
+ has $.id; ## Primary key, as per rules above.
+ has $.name is rw; ## The user's real name.
+ has $.age is rw; ## The user's age, in years.
+ has $.month is rw; ## The user's birth month.
+ has $.day is rw; ## The user's birth day.
+ has $.details is rw; ## Extra details, as a Hash.
+
+ ## We store details as a JSON string.
+ method load-details ($json) {
+ from-json($json);
+ }
+ method save-details {
+ to-json($!details);
+ }
+
+ ## We calculate the year based on age.
+ method year {
+ Date.today.year - $!age;
+ }
+
+ }
+
+ class Users does Oharem::Table {
+
+ has $!rows = User; ## The class to be used for Rows.
+ has $.table = 'users'; ## The database table this represents.
+
+ }
+
+ ## Must be passed a DBH object from a DBI-compatible library.
+ my $users = Users.new($dbh);
+ my $date = Date.now;
+ my @bdays = $users.get(:month($date.month), :day($date.day));
+ for @bdays -> $user {
+ $user.age++;
+ say "Happy birthday {$user.name}, born today back in {$user.year}!";
+ $user.save; ## Save the changes we made.
+ }
+ ## Okay, now let's create a user:
+ my $newuser = User.new(:name<Tim>, :age<32>, :month(6), :day(22));
+ $newuser.details = { src => 'https://github.com/supernovus' };
+ $users.add: $newuser;
+
+You get the idea.
+
View
23 docs/notes.txt → doc/old-notes.txt
@@ -1,26 +1,9 @@
OOSQL Notes
-----------------
-Taken from when this was WW::Model::Table, ignore some of the details.
-
-An extended Database Model role that will let you do something like:
-
- class Models::MyTable does WW::Model::Table {
- has $!table = 'mytable';
- has %!fields = { :id(:auto), :name(:required), :job };
- method change-name ($id, $name) {
- self.where(:$id).update(:$name); ## yes, where always comes first.
- }
- }
-
-Then in your Controller:
-
- self.load-model('MyTable').connect($db_connect_params);
- self.mytable.change-name(14, 'Bob'); ## change id 14's name to 'Bob';
- @results = self.mytable.where(:id(:gt(15))).limit(10).select(:name);
- for @results -> $result {
- $.data<users><name> = $result<name>;
- }
+Taken from when this was WW::Model::Table. I've pulled out the obsolete
+stuff, and made a new document outlining a future project that will use
+Esquel::DBI to implement two Roles making a full Database ORM possible.
Modifiers such as where(), limit(), order-by(), group-by() and having()
MUST come BEFORE select(), update(), and delete() statements.
View
144 lib/OOSQL.pm → lib/Esquel.pm6
@@ -1,34 +1,39 @@
-## OOSQL: Allows you to create classes that represent a table
-## in an SQL-based database. One class per table. This is for
-## simple object oriented database obstraction, no cross-joins, etc.
-##
-## The class must define an attribute called $!table which must contain
-## a string, which is the name of the database table this object represents.
-##
-## It must also define a method called 'execute-sql()' which takes a string
-## of SQL and sends it to the active database. It must return an object (of
-## a real class, or an anonymous class) that has at least the following
-## methods or attributes (implementation is up to you):
-##
-## $result.errors contains mysql errors if they happened, otherwise Nil.
-## $result.count if supported, contains the number of rows affected, or Nil.
-## $result.rows the returned rows from select, otherwise empty array.
-##
-## Read the role to see what attributes and methods it defines.
+## Esquel: Turn Perl 6 method calls and data structures into SQL queries.
-role OOSQL;
+class Esquel;
-has $!keep is rw; ## If set to true, the below items aren't cleared.
-has $!where is rw;
-has $!limit is rw; ## TODO
-has $!orderby is rw; ## TODO
-has $!groupby is rw; ## TODO
-has $!having is rw; ## TODO
+## These are set and not cleared by the clear() command, or by auto-clear.
+has $.table is rw; ## The DB table, (re)set with "from".
+has $!keep is rw; ## Disable auto-clear (runs clear() after action methods.)
+has $!bind is rw; ## If set, we use prepared statements with bound params.
-## When using all named parameters, assume AND rules.
-## If values are array, they will be ORed in an inner statement.
-multi method where (*%rules) {
- $!where = self!parse-where(%rules);
+## Stuff below here will be cleared by clear(:all) or by auto-clear.
+has $!where is rw; ## The WHERE clause for "select", "update", "delete".
+has @!bound is rw; ## Used by where is bind is true.
+has $!limit is rw; ## Limit to this many results.
+has $!orderby is rw; ## Order by...
+has $!groupby is rw; ## Group by...
+has $!having is rw; ## Similar to WHERE but used with aggregate functions.
+
+## Set the database table.
+method from ($table) {
+ $!table = $table;
+}
+
+## Change the auto-clear settings.
+method keep {
+ $!keep = True;
+}
+method nokeep {
+ $!keep = False;
+}
+
+## Change the bind settings.
+method bind {
+ $!bind = True;
+}
+method nobind {
+ $!bind = False;
}
## When using positional parameters, it's either a single string
@@ -39,15 +44,37 @@ multi method where (*%rules) {
## will generate:
## WHERE ((id = 15) OR (id = 17)) OR
## ((type = "admin") AND (job LIKE "manage"))
-multi method where (*@rules) {
+##
+## TODO <low priority> Add optional explicit AND/OR modifiers.
+method !query ($type, *@rules) {
if @rules.elems == 1 && @rules[0] ~~ Str {
- $!where = @rules[0];
+ return @rules[0];
}
else {
- $!where = self!parse-where(|@rules);
+ return $type ~ ' ' ~ self!parse-query(|@rules);
}
}
+## WHERE clause
+multi method where (*%rules) {
+ $!where = self!query('WHERE', %rules);
+ return self;
+}
+multi method where (*@rules) {
+ $!where = self!query('WHERE', |@rules);
+ return self;
+}
+
+## HAVING clause
+multi method having (*%rules) {
+ $!having = self!query('HAVING', %rules);
+ return self;
+}
+multi method having (*@rules) {
+ $!having = self!query('HAVING', |@rules);
+ return self;
+}
+
## parse-query: private method to parse where and having clauses.
method !parse-query (*@rules) {
my $where;
@@ -110,6 +137,11 @@ method !parse-query-statement ($key, $val) {
$want = "'$want'";
}
+ if $!bind {
+ @!bound.push: $want;
+ $want = '?';
+ }
+
my $statement = "$key $comp $want";
return $statement;
}
@@ -132,30 +164,58 @@ method !parse-aggregate-key($key is copy, $def) {
## Normal usage: $this.clear(:where, :having); # clears where and having.
## Keep usage: $this.clear(:keep, :where); # clears everything but where.
## All usage: $this.clear(:all); # clears everything, no exceptions.
-method clear (:$keep, :$all, *%mods) {
+method clear (:$keep, :$all, :$check, *%mods) {
+ if $check && $!keep { return; } ## Skip if auto-clear is turned off.
my %clear-rules = {
'where' => sub { undefine($!where); },
'limit' => sub { undefine($!limit); },
'having' => sub { undefine($!having); },
'orderby' => sub { undefine($!orderby); },
'groupby' => sub { undefine($!groupby); },
+ 'bound' => sub { @!bound = (); }, ## splice when it works again.
};
for %clear-rules.kv -> $rule, &clearit {
- if self!check-clear(%mods, $rule, :$keep, :$all) { clearit(); }
+ if ( $all || ($keep && !%mods{$which}) || (!$keep && %mods{$which})) {
+ clearit();
+ }
}
}
-## Should we clear this or not?
-method !check-clear (%mods, $which, :$keep, :$all) {
- ( $all || ($keep && !%mods{$which}) || (!$keep && %mods{$which}))
-}
-
## Represents a select query.
method select (*%fields, *@fields) {
- @fields.push: %fields.pairs;
- my $select;
- for @fields -> $field {
- ... ## TO BE FINISHED
+ if %fields {
+ @fields.push: %fields.pairs;
+ }
+
+ my $select = 'SELECT ';
+ if @fields.elems == 0 || @fields[0] ~~ Whatever || @fields[0] eq '*' {
+ $select ~= '*';
+ }
+ else {
+ my $comma = False;
+ for @fields -> $field {
+ my $name = $field;
+ if $field ~~ Pair {
+ $name = $field.key;
+ ## TODO: aggregate methods, AS statements, etc.
+ }
+ if $comma { $select ~= ','; }
+ else { $comma = True; }
+ $select ~= "$name";
+ }
+ }
+ $select ~= " FROM $!table";
+ if $!where {
+ $select ~= " $!where";
+ }
+ ## TODO: GROUP BY and ORDER BY
+ ## TODO: HAVING
+ my $return = [ $select, @!bound ];
+ my @bound = @!bound; ## Save prior to clearing.
+ self.clear(:check, :all);
+ if $!bind {
+ return $select, @bound;
}
+ return $select;
}

0 comments on commit 04f52f6

Please sign in to comment.
Something went wrong with that request. Please try again.