SQLite producer rewrite
[dbsrgits/SQL-Translator-2.0-ish.git] / lib / SQL / Translator / Producer / SQL / SQLite.pm
CommitLineData
e287b229 1use MooseX::Declare;
4f4fd192 2role SQL::Translator::Producer::SQL::SQLite {
55bb45c1 3 use MooseX::Types::Moose qw(HashRef);
4 use SQL::Translator::Constants qw(:sqlt_types :sqlt_constants);
4f4fd192 5 use SQL::Translator::Types qw(Column Table);
a5c5cd44 6
7 around _build_data_type_mapping {
8 my $data_type_mapping = $self->$orig;
9 $data_type_mapping->{SQL_FLOAT()} = 'real';
10 $data_type_mapping->{SQL_BIGINT()} = 'integer';
11
12 return $data_type_mapping;
13 };
55bb45c1 14
15sub header_comment {
16 my $producer = shift || caller;
17 my $comment_char = shift;
18 my $now = scalar localtime;
19my $DEFAULT_COMMENT = '-- ';
20
21 $comment_char = $DEFAULT_COMMENT
22 unless defined $comment_char;
23
24 my $header_comment =<<"HEADER_COMMENT";
25${comment_char}
26${comment_char}Created by $producer
27${comment_char}Created on $now
28${comment_char}
29HEADER_COMMENT
30
31 # Any additional stuff passed in
32 for my $additional_comment (@_) {
33 $header_comment .= "${comment_char}${additional_comment}\n";
34 }
35
36 return $header_comment;
37}
38
39method produce {
40 my $translator = $self->translator;
41# local $DEBUG = $translator->debug;
42# local $WARN = $translator->show_warnings;
43 my $no_comments = $translator->no_comments;
44 my $add_drop_table = $translator->add_drop_table;
45 my $schema = $translator->schema;
46 my $producer_args = $translator->producer_args;
47 my $sqlite_version = $producer_args->{sqlite_version} || 0;
48 my $no_txn = $producer_args->{no_transaction};
49
50# debug("PKG: Beginning production\n");
51
52# %global_names = (); #reset
53
54 my @create = ();
55 push @create, header_comment unless ($no_comments);
56 $create[0] .= "\n\nBEGIN TRANSACTION" unless $no_txn;
57
58 for my $table ( $schema->get_tables ) {
59 push @create, $self->create_table($table, { no_comments => $no_comments,
60 sqlite_version => $sqlite_version,
61 add_drop_table => $add_drop_table,});
62 }
63
64 for my $view ( $schema->get_views ) {
65 push @create, create_view($view, {
66 add_drop_view => $add_drop_table,
67 no_comments => $no_comments,
68 });
69 }
70
71 for my $trigger ( $schema->get_triggers ) {
72 push @create, create_trigger($trigger, {
73 add_drop_trigger => $add_drop_table,
74 no_comments => $no_comments,
75 });
76 }
77
78 if (wantarray) {
79 push @create, "COMMIT" unless $no_txn;
80 return @create;
81 } else {
82 push @create, "COMMIT;\n" unless $no_txn;
83 return join(";\n\n", @create );
84 }
85}
86
87# -------------------------------------------------------------------
88sub mk_name {
89 my ($name, $scope, $critical) = @_;
90my $max_id_length = 30;
91# $scope ||= \%global_names;
92 if ( my $prev = $scope->{ $name } ) {
93 my $name_orig = $name;
94 $name .= sprintf( "%02d", ++$prev );
95 substr($name, $max_id_length - 3) = "00"
96 if length( $name ) > $max_id_length;
97
98# warn "The name '$name_orig' has been changed to ",
99# "'$name' to make it unique.\n" if $WARN;
100
101 $scope->{ $name_orig }++;
102 }
103
104 $scope->{ $name }++;
105 return $name;
106}
107
108sub create_view {
109 my ($view, $options) = @_;
110 my $add_drop_view = $options->{add_drop_view};
111
112 my $view_name = $view->name;
113# debug("PKG: Looking at view '${view_name}'\n");
114
115 # Header. Should this look like what mysqldump produces?
116 my $extra = $view->extra;
117 my $create = '';
118 $create .= "--\n-- View: ${view_name}\n--\n" unless $options->{no_comments};
119 $create .= "DROP VIEW IF EXISTS $view_name;\n" if $add_drop_view;
120 $create .= 'CREATE';
121 $create .= " TEMPORARY" if exists($extra->{temporary}) && $extra->{temporary};
122 $create .= ' VIEW';
123 $create .= " IF NOT EXISTS" if exists($extra->{if_not_exists}) && $extra->{if_not_exists};
124 $create .= " ${view_name}";
125
126 if( my $sql = $view->sql ){
127 $create .= " AS\n ${sql}";
128 }
129 return $create;
130}
131
132
133#sub create_table
134#{
135# my ($table, $options) = @_;
136method create_table(Table $table, HashRef $options) {
137 my $table_name = $table->name;
138 my $no_comments = $options->{no_comments};
139 my $add_drop_table = $options->{add_drop_table};
140 my $sqlite_version = $options->{sqlite_version} || 0;
141
142# debug("PKG: Looking at table '$table_name'\n");
143
144 my ( @index_defs, @constraint_defs );
145 my @fields = $table->get_fields or die "No fields in $table_name";
146
147 my $temp = $options->{temporary_table} ? 'TEMPORARY ' : '';
148 #
149 # Header.
150 #
151 my $exists = ($sqlite_version >= 3.3) ? ' IF EXISTS' : '';
152 my @create;
153 my ($comment, $create_table) = "";
154 $comment = "--\n-- Table: $table_name\n--\n" unless $no_comments;
155 if ($add_drop_table) {
156 push @create, $comment . qq[DROP TABLE$exists $table_name];
157 } else {
158 $create_table = $comment;
159 }
160
161 $create_table .= "CREATE ${temp}TABLE $table_name (\n";
162
163 #
164 # Comments
165 #
166 if ( $table->comments and !$no_comments ){
167 $create_table .= "-- Comments: \n-- ";
168 $create_table .= join "\n-- ", $table->comments;
169 $create_table .= "\n--\n\n";
170 }
171
172 #
173 # How many fields in PK?
174 #
175 my $pk = $table->primary_key;
176 my @pk_fields = $pk ? $pk->fields : ();
177
178 #
179 # Fields
180 #
181 my ( @field_defs, $pk_set );
182 for my $field ( @fields ) {
183 push @field_defs, create_field($field);
184 }
185
186 if (
187 scalar @pk_fields > 1
188 ||
189 ( @pk_fields && !grep /INTEGER PRIMARY KEY/, @field_defs )
190 ) {
191 push @field_defs, 'PRIMARY KEY (' . join(', ', @pk_fields ) . ')';
192 }
193
194 #
195 # Indices
196 #
197 my $idx_name_default = 'A';
198 for my $index ( $table->get_indices ) {
199 push @index_defs, create_index($index);
200 }
201
202 #
203 # Constraints
204 #
205 my $c_name_default = 'A';
206 for my $c ( $table->get_constraints ) {
207 next unless $c->type eq UNIQUE;
208 push @constraint_defs, create_constraint($c);
209 }
210
211 $create_table .= join(",\n", map { " $_" } @field_defs ) . "\n)";
212
213 return (@create, $create_table, @index_defs, @constraint_defs );
214}
215
216sub create_field
217{
218 my ($field, $options) = @_;
219
220 my $field_name = $field->name;
221# debug("PKG: Looking at field '$field_name'\n");
222 my $field_comments = $field->comments
223 ? "-- " . $field->comments . "\n "
224 : '';
225
226 my $field_def = $field_comments.$field_name;
227
228 # data type and size
229 my $size = $field->size;
230 my $data_type = $field->data_type;
231 $data_type = 'varchar' if lc $data_type eq 'set';
232 $data_type = 'blob' if lc $data_type eq 'bytea';
233
234 if ( lc $data_type =~ /(text|blob)/i ) {
235 $size = undef;
236 }
237
238# if ( $data_type =~ /timestamp/i ) {
239# push @trigger_defs,
240# "CREATE TRIGGER ts_${table_name} ".
241# "after insert on $table_name\n".
242# "begin\n".
243# " update $table_name set $field_name=timestamp() ".
244# "where id=new.id;\n".
245# "end;\n"
246# ;
247#
248# }
249
250 #
251 # SQLite is generally typeless, but newer versions will
252 # make a field autoincrement if it is declared as (and
253 # *only* as) INTEGER PRIMARY KEY
254 #
255 my $pk = $field->table->primary_key;
256 my @pk_fields = $pk ? $pk->fields : ();
257
258 if (
259 $field->is_primary_key &&
260 scalar @pk_fields == 1 &&
261 (
262 $data_type =~ /int(eger)?$/i
263 ||
264 ( $data_type =~ /^number?$/i && $size !~ /,/ )
265 )
266 ) {
267 $data_type = 'INTEGER PRIMARY KEY';
268 $size = undef;
269# $pk_set = 1;
270 }
271
272 $field_def .= sprintf " %s%s", $data_type,
273 ( !$field->is_auto_increment && $size ) ? "($size)" : '';
274
275 # Null?
276 $field_def .= ' NOT NULL' unless $field->is_nullable;
277
278 # Default? XXX Need better quoting!
279 my $default = $field->default_value;
280=cut
281 if (defined $default) {
282 SQL::Translator::Producer->_apply_default_value(
283 \$field_def,
284 $default,
285 [
286 'NULL' => \'NULL',
287 'now()' => 'now()',
288 'CURRENT_TIMESTAMP' => 'CURRENT_TIMESTAMP',
289 ],
290 );
291 }
292=cut
293
294 return $field_def;
295
296}
297
298sub create_index
299{
300 my ($index, $options) = @_;
301
302 my $name = $index->name;
303 $name = mk_name($name);
304
305 my $type = $index->type eq 'UNIQUE' ? "UNIQUE " : '';
306
307 # strip any field size qualifiers as SQLite doesn't like these
308 my @fields = map { s/\(\d+\)$//; $_ } $index->fields;
309 (my $index_table_name = $index->table->name) =~ s/^.+?\.//; # table name may not specify schema
310# warn "removing schema name from '" . $index->table->name . "' to make '$index_table_name'\n" if $WARN;
311 my $index_def =
312 "CREATE ${type}INDEX $name ON " . $index_table_name .
313 ' (' . join( ', ', @fields ) . ')';
314
315 return $index_def;
316}
317
318sub create_constraint
319{
320 my ($c, $options) = @_;
321
322 my $name = $c->name;
323 $name = mk_name($name);
324 my @fields = $c->fields;
325 (my $index_table_name = $c->table->name) =~ s/^.+?\.//; # table name may not specify schema
326# warn "removing schema name from '" . $c->table->name . "' to make '$index_table_name'\n" if $WARN;
327
328 my $c_def =
329 "CREATE UNIQUE INDEX $name ON " . $index_table_name .
330 ' (' . join( ', ', @fields ) . ')';
331
332 return $c_def;
333}
334
335sub create_trigger {
336 my ($trigger, $options) = @_;
337 my $add_drop = $options->{add_drop_trigger};
338
339 my @statements;
340
341 my $trigger_name = $trigger->name;
342 my $events = $trigger->database_events;
343 for my $evt ( @$events ) {
344
345 my $trig_name = $trigger_name;
346 if (@$events > 1) {
347 $trig_name .= "_$evt";
348
349# warn "Multiple database events supplied for trigger '$trigger_name', ",
350# "creating trigger '$trig_name' for the '$evt' event.\n" if $WARN;
351 }
352
353 push @statements, "DROP TRIGGER IF EXISTS $trig_name" if $add_drop;
354
355
356 $DB::single = 1;
357 my $action = "";
358 if (not ref $trigger->action) {
359 $action .= "BEGIN " . $trigger->action . " END";
360 }
361 else {
362 $action = $trigger->action->{for_each} . " "
363 if $trigger->action->{for_each};
364
365 $action = $trigger->action->{when} . " "
366 if $trigger->action->{when};
367
368 my $steps = $trigger->action->{steps} || [];
369
370 $action .= "BEGIN ";
371 $action .= $_ . "; " for (@$steps);
372 $action .= "END";
4f4fd192 373 }
55bb45c1 374
375 push @statements, sprintf (
376 'CREATE TRIGGER %s %s %s on %s %s',
377 $trig_name,
378 $trigger->perform_action_when,
379 $evt,
380 $trigger->on_table,
381 $action
382 );
383 }
384
385 return @statements;
386}
387
388sub alter_table { } # Noop
389
390sub add_field {
391 my ($field) = @_;
392
393 return sprintf("ALTER TABLE %s ADD COLUMN %s",
394 $field->table->name, create_field($field))
395}
396
397sub alter_create_index {
398 my ($index) = @_;
399
400 # This might cause name collisions
401 return create_index($index);
402}
403
404sub alter_create_constraint {
405 my ($constraint) = @_;
406
407 return create_constraint($constraint) if $constraint->type eq 'UNIQUE';
408}
409
410sub alter_drop_constraint { alter_drop_index(@_) }
411
412sub alter_drop_index {
413 my ($constraint) = @_;
414
415 return sprintf("DROP INDEX %s",
416 $constraint->name);
417}
418
419sub batch_alter_table {
420 my ($table, $diffs) = @_;
421
422 # If we have any of the following
423 #
424 # rename_field
425 # alter_field
426 # drop_field
427 #
428 # we need to do the following <http://www.sqlite.org/faq.html#q11>
429 #
430 # BEGIN TRANSACTION;
431 # CREATE TEMPORARY TABLE t1_backup(a,b);
432 # INSERT INTO t1_backup SELECT a,b FROM t1;
433 # DROP TABLE t1;
434 # CREATE TABLE t1(a,b);
435 # INSERT INTO t1 SELECT a,b FROM t1_backup;
436 # DROP TABLE t1_backup;
437 # COMMIT;
438 #
439 # Fun, eh?
440 #
441 # If we have rename_field we do similarly.
442
443 my $table_name = $table->name;
444 my $renaming = $diffs->{rename_table} && @{$diffs->{rename_table}};
445
446 if ( @{$diffs->{rename_field}} == 0 &&
447 @{$diffs->{alter_field}} == 0 &&
448 @{$diffs->{drop_field}} == 0
449 ) {
450# return join("\n", map {
451 return map {
452 my $meth = __PACKAGE__->can($_) or die __PACKAGE__ . " cant $_";
453 map { my $sql = $meth->(ref $_ eq 'ARRAY' ? @$_ : $_); $sql ? ("$sql") : () } @{ $diffs->{$_} }
454
455 } grep { @{$diffs->{$_}} }
456 qw/rename_table
457 alter_drop_constraint
458 alter_drop_index
459 drop_field
460 add_field
461 alter_field
462 rename_field
463 alter_create_index
464 alter_create_constraint
465 alter_table/;
466 }
467
468
469 my @sql;
470 my $old_table = $renaming ? $diffs->{rename_table}[0][0] : $table;
471
472 do {
473 local $table->{name} = $table_name . '_temp_alter';
474 # We only want the table - dont care about indexes on tmp table
475 my ($table_sql) = create_table($table, {no_comments => 1, temporary_table => 1});
476 push @sql,$table_sql;
477 };
478
479 push @sql, "INSERT INTO @{[$table_name]}_temp_alter SELECT @{[ join(', ', $old_table->get_fields)]} FROM @{[$old_table]}",
480 "DROP TABLE @{[$old_table]}",
481 create_table($table, { no_comments => 1 }),
482 "INSERT INTO @{[$table_name]} SELECT @{[ join(', ', $old_table->get_fields)]} FROM @{[$table_name]}_temp_alter",
483 "DROP TABLE @{[$table_name]}_temp_alter";
484
485 return @sql;
486# return join("", @sql, "");
487}
488
489sub drop_table {
490 my ($table) = @_;
491 return "DROP TABLE $table";
492}
493
494sub rename_table {
495 my ($old_table, $new_table, $options) = @_;
496
497 my $qt = $options->{quote_table_names} || '';
498
499 return "ALTER TABLE $qt$old_table$qt RENAME TO $qt$new_table$qt";
500
501}
502
503# No-op. Just here to signify that we are a new style parser.
504sub preproces_schema { }
287d4603 505}