diff --git a/.gitignore b/.gitignore index 5fb5749..ebbe76e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ SQL-Formatter-* /.build/ *.swp - - +/ffi/target +/ffi/_build +/ffi/Cargo.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..b21559c --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# SQL::Formatter ![static](https://github.com/uperl/SQL-Formatter/workflows/static/badge.svg) ![linux](https://github.com/uperl/SQL-Formatter/workflows/linux/badge.svg) + +Format SQL using the rust sqlformat library + +# SYNOPSIS + +```perl +my $f = SQL::Formatter->new; +say $f->format('select foo.a, foo.b, bar.c from foo join bar on foo.a = bar.c where foo.b = 2'); +``` + +prints: + +``` +SELECT + foo.a, + foo.b, + bar.c +FROM + foo + JOIN bar ON foo.a = bar.c +WHERE + foo.b = 2 +``` + +# DESCRIPTION + +Pretty print SQL using the rust crate `sqlformat`. + +# ATTRIBUTES + +The formatting options can be specified either when the object is constructed, or later using accessors. + +```perl +my $f = SQL::Format->new( indent => 4 ); +$f->indent(4); +``` + +- indent + + Controls the length of indentation to use. The default is `2`. + +- uppercase + + When set to true (the default), changes reserved keywords to ALL CAPS. + +- lines\_between\_queries + + Controls the number of line breaks after a query. The default is `1`. + +# METHODS + +## format + +```perl +my $pretty_sql = $f->format($sql); +``` + +Formats whitespace in a SQL string to make it easier to read. + +# AUTHOR + +Graham Ollis + +# COPYRIGHT AND LICENSE + +This software is copyright (c) 2024 by Graham Ollis. + +This is free software; you can redistribute it and/or modify it under +the same terms as the Perl 5 programming language system itself. diff --git a/author.yml b/author.yml index 40420cc..130578b 100644 --- a/author.yml +++ b/author.yml @@ -5,7 +5,8 @@ pod_spelling_system: # (regardless of what spell check thinks) # or stuff that I like to spell incorrectly # intentionally - stopwords: [] + stopwords: + - sqlformat pod_coverage: skip: 0 diff --git a/dist.ini b/dist.ini index 0f1701c..d52c27b 100644 --- a/dist.ini +++ b/dist.ini @@ -21,4 +21,9 @@ version_plugin = PkgVersion::Block [Author::Plicease::Upload] cpan = 0 +[FFI::Build] +lang = Rust +build = Cargo +[PruneFiles] +filename = ffi/Cargo.lock diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml new file mode 100644 index 0000000..1081e65 --- /dev/null +++ b/ffi/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sf" +version = "0.1.0" +edition = "2021" + +[dependencies] +sqlformat = "0.2.6" + +[lib] +crate-type = ["cdylib"] diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs new file mode 100644 index 0000000..16a58f3 --- /dev/null +++ b/ffi/src/lib.rs @@ -0,0 +1,26 @@ +use std::ffi::CString; +use std::ffi::CStr; +use sqlformat::format; +use std::os::raw::c_char; + +#[no_mangle] +pub fn sf_format(query: *const c_char, indent: u8, uppercase: bool, between: u8) -> *mut c_char { + let query = unsafe { CStr::from_ptr(query) }; + let options = sqlformat::FormatOptions { + indent: sqlformat::Indent::Spaces(indent), + uppercase: uppercase, + lines_between_queries: between, + }; + let query = format(query.to_str().unwrap(), &sqlformat::QueryParams::default(), options); + + CString::new(query).unwrap().into_raw() +} + +#[allow(non_snake_case)] +#[no_mangle] +pub fn sf__free(s: *mut c_char) { + if s.is_null() { + } else { + unsafe { drop(CString::from_raw(s)) }; + } +} diff --git a/lib/SQL/Formatter.pm b/lib/SQL/Formatter.pm index 3238136..125bd92 100644 --- a/lib/SQL/Formatter.pm +++ b/lib/SQL/Formatter.pm @@ -5,7 +5,81 @@ use experimental qw( postderef signatures ); package SQL::Formatter { # ABSTRACT: Format SQL using the rust sqlformat library + +=head1 SYNOPSIS + + my $f = SQL::Formatter->new; + say $f->format('select foo.a, foo.b, bar.c from foo join bar on foo.a = bar.c where foo.b = 2'); + +prints: + + SELECT + foo.a, + foo.b, + bar.c + FROM + foo + JOIN bar ON foo.a = bar.c + WHERE + foo.b = 2 + +=head1 DESCRIPTION + +Pretty print SQL using the rust crate C. + +=head1 ATTRIBUTES + +The formatting options can be specified either when the object is constructed, or later using accessors. + + my $f = SQL::Format->new( indent => 4 ); + $f->indent(4); + +=over 4 + +=item indent + +Controls the length of indentation to use. The default is C<2>. + +=item uppercase + +When set to true (the default), changes reserved keywords to ALL CAPS. + +=item lines_between_queries + +Controls the number of line breaks after a query. The default is C<1>. + +=back + +=head1 METHODS + +=head2 format + + my $pretty_sql = $f->format($sql); + +Formats whitespace in a SQL string to make it easier to read. + +=cut + + use FFI::Platypus 2.00; + use Class::Tiny { + indent => 2, + uppercase => 1, + lines_between_queries => 1 + }; + + my $ffi = FFI::Platypus->new( api => 2, lang => 'Rust' ); + $ffi->bundle; + $ffi->mangler(sub ($name) { "sf_$name" }); + + $ffi->attach( _free => ['opaque'] ); + + $ffi->attach( format => ['string','u8','bool','u8'] => 'opaque' => sub ($xsub, $self, $sql) { + my $ptr = $xsub->($sql, $self->indent, $self->uppercase, $self->lines_between_queries); + my $str = $ffi->cast( 'opaque' => 'string', $ptr ); + _free($ptr); + return $str; + }); + } 1; - diff --git a/t/00_diag.t b/t/00_diag.t new file mode 100644 index 0000000..2b178c4 --- /dev/null +++ b/t/00_diag.t @@ -0,0 +1,91 @@ +use Test2::V0 -no_srand => 1; +use Config; + +eval { require 'Test/More.pm' }; + +# This .t file is generated. +# make changes instead to dist.ini + +my %modules; +my $post_diag; + +$modules{$_} = $_ for qw( + Class::Tiny + ExtUtils::MakeMaker + FFI::Build::File::Cargo + FFI::Build::MM + FFI::Platypus + FFI::Platypus::Lang::Rust + Test2::V0 +); + + + +my @modules = sort keys %modules; + +sub spacer () +{ + diag ''; + diag ''; + diag ''; +} + +pass 'okay'; + +my $max = 1; +$max = $_ > $max ? $_ : $max for map { length $_ } @modules; +our $format = "%-${max}s %s"; + +spacer; + +my @keys = sort grep /(MOJO|PERL|\A(LC|HARNESS)_|\A(SHELL|LANG)\Z)/i, keys %ENV; + +if(@keys > 0) +{ + diag "$_=$ENV{$_}" for @keys; + + if($ENV{PERL5LIB}) + { + spacer; + diag "PERL5LIB path"; + diag $_ for split $Config{path_sep}, $ENV{PERL5LIB}; + + } + elsif($ENV{PERLLIB}) + { + spacer; + diag "PERLLIB path"; + diag $_ for split $Config{path_sep}, $ENV{PERLLIB}; + } + + spacer; +} + +diag sprintf $format, 'perl', "$] $^O $Config{archname}"; + +foreach my $module (sort @modules) +{ + my $pm = "$module.pm"; + $pm =~ s{::}{/}g; + if(eval { require $pm; 1 }) + { + my $ver = eval { $module->VERSION }; + $ver = 'undef' unless defined $ver; + diag sprintf $format, $module, $ver; + } + else + { + diag sprintf $format, $module, '-'; + } +} + +if($post_diag) +{ + spacer; + $post_diag->(); +} + +spacer; + +done_testing; + diff --git a/t/sql_formatter.t b/t/sql_formatter.t index dcaf944..d575617 100644 --- a/t/sql_formatter.t +++ b/t/sql_formatter.t @@ -1,7 +1,18 @@ use Test2::V0 -no_srand => 1; use SQL::Formatter; -ok 1, 'todo'; +my $f = SQL::Formatter->new; + +my @ret; +is( + [@ret=$f->format("select x, y, z from foo join bar where x = 1 and y = 2")], + [D()], + 'does something at least' +); + +note @ret; + +note $f->format('select foo.a, foo.b, bar.c from foo join bar on foo.a = bar.c where foo.b = 2'); done_testing;