src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java
public FeedLoadResult exportTables() {
        // This result object will be returned to the caller to summarize the feed and report any critical errors.
        // FIXME: use feed export result object?
        FeedLoadResult result = new FeedLoadResult();

        try {
            zipOutputStream = new ZipOutputStream(new FileOutputStream(outFile));
            long startTime = System.currentTimeMillis();
            // We get a single connection object and share it across several different methods.
            // This ensures that actions taken in one method are visible to all subsequent SQL statements.
            // If we create a schema or table on one connection, then access it in a separate connection, we have no
            // guarantee that it exists when the accessing statement is executed.
            connection = dataSource.getConnection();
            if (!connection.getMetaData().getDatabaseProductName().equals("PostgreSQL")) {
                // This code path currently requires the Postgres text "copy to" format.
                throw new RuntimeException("Export from SQL database not implemented for non-PostgreSQL databases.");
            }
            // Construct where clause for routes table to filter only "approved" routes (and entities related to routes)
            // if exporting a feed/schema that represents an editor snapshot.
            String whereRouteIsApproved = String.format("where %s.%s.status = 2", feedIdToExport, Table.ROUTES.name);
            // Export each table in turn (by placing entry in zip output stream).
            result.agency = export(Table.AGENCY, connection);
            if (fromEditor) {
                // only export calendar entries that have at least one day of service set
                // this could happen in cases where a feed was imported that only had calendar_dates.txt
                result.calendar = export(
                    Table.CALENDAR,
                    String.join(
                        " ",
                        Table.CALENDAR.generateSelectSql(feedIdToExport, Requirement.OPTIONAL),
                        "WHERE monday=1 OR tuesday=1 OR wednesday=1 OR thursday=1 OR friday=1 OR saturday=1 OR sunday=1"
                    )
                );
            } else {
                result.calendar = export(Table.CALENDAR, connection);
            }
            if (fromEditor) {
                // Export schedule exceptions in place of calendar dates if exporting a feed/schema that represents an editor snapshot.
                GTFSFeed feed = new GTFSFeed();
                // FIXME: The below table readers should probably just share a connection with the exporter.
                JDBCTableReader<ScheduleException> exceptionsReader =
                        new JDBCTableReader(Table.SCHEDULE_EXCEPTIONS, dataSource, feedIdToExport + ".",
                                EntityPopulator.SCHEDULE_EXCEPTION);
                JDBCTableReader<Calendar> calendarsReader =
                        new JDBCTableReader(Table.CALENDAR, dataSource, feedIdToExport + ".",
                                EntityPopulator.CALENDAR);
                Iterable<Calendar> calendars = calendarsReader.getAll();
                Iterable<ScheduleException> exceptionsIterator = exceptionsReader.getAll();
                List<ScheduleException> exceptions = new ArrayList<>();
                // FIXME: Doing this causes the connection to stay open, but it is closed in the finalizer so it should
                // not be a big problem.
                for (ScheduleException exception : exceptionsIterator) {
                    exceptions.add(exception);
                }
                // check whether the feed is organized in a format with the calendars.txt file
                if (calendarsReader.getRowCount() > 0) {
                    // feed does have calendars.txt file, continue export with strategy of matching exceptions
                    // to calendar to output calendar_dates.txt
                    int calendarDateCount = 0;
                    for (Calendar cal : calendars) {
                        Service service = new Service(cal.service_id);
                        service.calendar = cal;
                        for (ScheduleException ex : exceptions) {
                            if (ex.exemplar.equals(ScheduleException.ExemplarServiceDescriptor.SWAP) &&
                                (!ex.addedService.contains(cal.service_id) && !ex.removedService.contains(cal.service_id))) {
                                // Skip swap exception if cal is not referenced by added or removed service.
                                // This is not technically necessary, but the output is cleaner/more intelligible.
                                continue;
                            }

                            for (LocalDate date : ex.dates) {
                                if (date.isBefore(cal.start_date) || date.isAfter(cal.end_date)) {
                                    // No need to write dates that do not apply
                                    continue;
                                }

                                CalendarDate calendarDate = new CalendarDate();
                                calendarDate.date = date;
                                calendarDate.service_id = cal.service_id;
                                calendarDate.exception_type = ex.serviceRunsOn(cal) ? 1 : 2;
                                LOG.info("Adding exception {} (type={}) for calendar {} on date {}", ex.name, calendarDate.exception_type, cal.service_id, date.toString());

                                if (service.calendar_dates.containsKey(date))
                                    throw new IllegalArgumentException("Duplicate schedule exceptions on " + date.toString());

                                service.calendar_dates.put(date, calendarDate);
                                calendarDateCount += 1;
                            }
                        }
                        feed.services.put(cal.service_id, service);
                    }
                    if (calendarDateCount == 0) {
                        LOG.info("No calendar dates found. Skipping table.");
                    } else {
                        LOG.info("Writing {} calendar dates from schedule exceptions", calendarDateCount);
                        new CalendarDate.Writer(feed).writeTable(zipOutputStream);
                    }
                } else {
                    // No calendar records exist, export calendar_dates as is and hope for the best.
                    // This situation will occur in at least 2 scenarios:
                    // 1.  A GTFS has been loaded into the editor that had only the calendar_dates.txt file
                    //     and no further edits were made before exporting to a snapshot
                    // 2.  A new GTFS has been created from scratch and calendar information has yet to be added.
                    //     This will result in an invalid GTFS, but it was what the user wanted so ¯\_(ツ)_/¯
                    result.calendarDates = export(Table.CALENDAR_DATES, connection);
                }
            } else {
                // Otherwise, simply export the calendar dates as they were loaded in.
                result.calendarDates = export(Table.CALENDAR_DATES, connection);
            }
            result.fareAttributes = export(Table.FARE_ATTRIBUTES, connection);
            result.fareRules = export(Table.FARE_RULES, connection);
            result.feedInfo = export(Table.FEED_INFO, connection);
            // Only write frequencies for "approved" routes using COPY TO with results of select query
            if (fromEditor) {
                // Generate filter SQL for trips if exporting a feed/schema that represents an editor snapshot.
                // The filter clause for frequencies requires two joins to reach the routes table and a where filter on
                // route status.
                // FIXME Replace with string literal query instead of clause generators
                result.frequencies = export(
                    Table.FREQUENCIES,
                    String.join(
                        " ",
                        // Convert start_time and end_time values from seconds to time format (HH:MM:SS).
                        Table.FREQUENCIES.generateSelectSql(feedIdToExport, Requirement.OPTIONAL),
                        Table.FREQUENCIES.generateJoinSql(Table.TRIPS, feedIdToExport),
                        Table.TRIPS.generateJoinSql(
                            Table.ROUTES,
                            feedIdToExport,
                            "route_id",
                            false
                        ),
                        whereRouteIsApproved
                    )
                );
            } else {
                result.frequencies = export(Table.FREQUENCIES, connection);
            }

            // Only write "approved" routes using COPY TO with results of select query
            if (fromEditor) {
                // The filter clause for routes is simple. We're just checking that the route is APPROVED.
                result.routes = export(
                    Table.ROUTES,
                    String.join(
                        " ",
                        Table.ROUTES.generateSelectSql(feedIdToExport, Requirement.OPTIONAL),
                        whereRouteIsApproved
                    )
                );
            } else {
                result.routes = export(Table.ROUTES, connection);
            }

            // FIXME: Find some place to store errors encountered on export for patterns and pattern stops.
            // FIXME: Is there a need to export patterns or pattern stops? Should these be iterated over to ensure that
            // frequency-based pattern travel times match stop time arrivals/departures?
//            export(Table.PATTERNS);
//            export(Table.PATTERN_STOP);
            // Only write shapes for "approved" routes using COPY TO with results of select query
            if (fromEditor) {
                // Generate filter SQL for shapes if exporting a feed/schema that represents an editor snapshot.
                // The filter clause for shapes requires joining to trips and then to routes table and a where filter on
                // route status.
                // FIXME: I'm not sure that shape_id is indexed for the trips table. This could cause slow downs.
                // FIXME: this is exporting point_type, which is not a GTFS field, but its presence shouldn't hurt.
                String shapeFieldsToExport = Table.commaSeparatedNames(
                    Table.SHAPES.specFields(),
                    String.join(".", feedIdToExport, Table.SHAPES.name + "."),
                    true
                );
                // NOTE: The below substitution uses relative indexing. All values "%<s" reference the same arg as the
                // previous format specifier (i.e., feedIdToExport).
                result.shapes = export(
                    Table.SHAPES,
                    String.format(
                        "select %s " +
                            "from (select distinct %s.trips.shape_id " +
                            "from %<s.trips, %<s.routes " +
                            "where %<s.trips.route_id = %<s.routes.route_id and " +
                            "%<s.routes.status = 2) as unique_approved_shape_ids, " +
                            "%<s.shapes where unique_approved_shape_ids.shape_id = %<s.shapes.shape_id",
                        shapeFieldsToExport,
                        feedIdToExport
                    )
                );
            } else {
                result.shapes = export(Table.SHAPES, connection);
            }
            result.stops = export(Table.STOPS, connection);
            // Only write stop times for "approved" routes using COPY TO with results of select query
            if (fromEditor) {
                // Generate filter SQL for trips if exporting a feed/schema that represents an editor snapshot.
                // The filter clause for stop times requires two joins to reach the routes table and a where filter on
                // route status.
                // FIXME Replace with string literal query instead of clause generators
                result.stopTimes = export(
                    Table.STOP_TIMES,
                    String.join(" ",
                        Table.STOP_TIMES.generateSelectSql(feedIdToExport, Requirement.OPTIONAL),
                        Table.STOP_TIMES.generateJoinSql(Table.TRIPS, feedIdToExport),
                        Table.TRIPS.generateJoinSql(
                            Table.ROUTES,
                            feedIdToExport,
                            "route_id",
                            false
                        ),
                        whereRouteIsApproved
                    )
                );
            } else {
                result.stopTimes = export(Table.STOP_TIMES, connection);
            }
            result.transfers = export(Table.TRANSFERS, connection);
            if (fromEditor) {
                // Generate filter SQL for trips if exporting a feed/schema that represents an editor snapshot.
                // The filter clause for trips requires an inner join on the routes table and the same where check on
                // route status.
                // FIXME Replace with string literal query instead of clause generators
                result.trips = export(
                    Table.TRIPS,
                    String.join(" ",
                        Table.TRIPS.generateSelectSql(feedIdToExport, Requirement.OPTIONAL),
                        Table.TRIPS.generateJoinSql(
                            Table.ROUTES,
                            feedIdToExport,
                            "route_id",
                            false
                        ),
                        whereRouteIsApproved
                    )
                );
            } else {
                result.trips = export(Table.TRIPS, connection);
            }

            zipOutputStream.close();
            // Run clean up on the resulting zip file.
            cleanUpZipFile();
            result.completionTime = System.currentTimeMillis();
            result.loadTimeMillis = result.completionTime - startTime;
            // Exporting primary GTFS tables for GRTA Xpress = 12 sec
            LOG.info("Exporting tables took {} sec", (result.loadTimeMillis) / 1000);
            LOG.info("Exported feed {} to zip file: {}", feedIdToExport, outFile);
        } catch (Exception ex) {
            // Note: Exceptions that occur during individual table loads are separately caught and stored in
            // TableLoadResult.
            LOG.error("Exception while creating snapshot: {}", ex.toString());
            ex.printStackTrace();
            result.fatalException = ex.toString();
        }
        return result;
    }
public FeedLoadResult exportTables() {
        // This result object will be returned to the caller to summarize the feed and report any critical errors.
        // FIXME: use feed export result object?
        FeedLoadResult result = new FeedLoadResult();

        try {
            zipOutputStream = new ZipOutputStream(new FileOutputStream(outFile));
            long startTime = System.currentTimeMillis();
            // We get a single connection object and share it across several different methods.
            // This ensures that actions taken in one method are visible to all subsequent SQL statements.
            // If we create a schema or table on one connection, then access it in a separate connection, we have no
            // guarantee that it exists when the accessing statement is executed.
            connection = dataSource.getConnection();
            if (!connection.getMetaData().getDatabaseProductName().equals("PostgreSQL")) {
                // This code path currently requires the Postgres text "copy to" format.
                throw new RuntimeException("Export from SQL database not implemented for non-PostgreSQL databases.");
            }
            // Construct where clause for routes table to filter only "approved" routes (and entities related to routes)
            // if exporting a feed/schema that represents an editor snapshot.
            String whereRouteIsApproved = String.format("where %s.%s.status = 2", feedIdToExport, Table.ROUTES.name);
            // Export each table in turn (by placing entry in zip output stream).
            result.agency = export(Table.AGENCY, connection);
            if (fromEditor) {
                // only export calendar entries that have at least one day of service set
                // this could happen in cases where a feed was imported that only had calendar_dates.txt
                result.calendar = export(
                    Table.CALENDAR,
                    String.join(
                        " ",
                        Table.CALENDAR.generateSelectSql(feedIdToExport, Requirement.OPTIONAL),
                        "WHERE monday=1 OR tuesday=1 OR wednesday=1 OR thursday=1 OR friday=1 OR saturday=1 OR sunday=1"
                    )
                );
            } else {
                result.calendar = export(Table.CALENDAR, connection);
            }
            if (fromEditor) {
                // Export schedule exceptions in place of calendar dates if exporting a feed/schema that represents an editor snapshot.
                GTFSFeed feed = new GTFSFeed();
                // FIXME: The below table readers should probably just share a connection with the exporter.
                JDBCTableReader<ScheduleException> exceptionsReader =
                        new JDBCTableReader(Table.SCHEDULE_EXCEPTIONS, dataSource, feedIdToExport + ".",
                                EntityPopulator.SCHEDULE_EXCEPTION);
                JDBCTableReader<Calendar> calendarsReader =
                        new JDBCTableReader(Table.CALENDAR, dataSource, feedIdToExport + ".",
                                EntityPopulator.CALENDAR);
                Iterable<Calendar> calendars = calendarsReader.getAll();
                Iterable<ScheduleException> exceptionsIterator = exceptionsReader.getAll();
                List<ScheduleException> exceptions = new ArrayList<>();
                // FIXME: Doing this causes the connection to stay open, but it is closed in the finalizer so it should
                // not be a big problem.
                for (ScheduleException exception : exceptionsIterator) {
                    exceptions.add(exception);
                }
                // check whether the feed is organized in a format with the calendars.txt file
                if (calendarsReader.getRowCount() > 0) {
                    // feed does have calendars.txt file, continue export with strategy of matching exceptions
                    // to calendar to output calendar_dates.txt
                    int calendarDateCount = 0;
                    for (Calendar cal : calendars) {
                        Service service = new Service(cal.service_id);
                        service.calendar = cal;
                        for (ScheduleException ex : exceptions) {
                            if (ex.exemplar.equals(ScheduleException.ExemplarServiceDescriptor.SWAP) &&
                                (!ex.addedService.contains(cal.service_id) && !ex.removedService.contains(cal.service_id))) {
                                // Skip swap exception if cal is not referenced by added or removed service.
                                // This is not technically necessary, but the output is cleaner/more intelligible.
                                continue;
                            }

                            for (LocalDate date : ex.dates) {
                                if (date.isBefore(cal.start_date) || date.isAfter(cal.end_date)) {
                                    // No need to write dates that do not apply
                                    continue;
                                }

                                CalendarDate calendarDate = new CalendarDate();
                                calendarDate.date = date;
                                calendarDate.service_id = cal.service_id;
                                calendarDate.exception_type = ex.serviceRunsOn(cal) ? 1 : 2;
                                LOG.info("Adding exception {} (type={}) for calendar {} on date {}", ex.name, calendarDate.exception_type, cal.service_id, date.toString());

                                if (service.calendar_dates.containsKey(date))
                                    throw new IllegalArgumentException("Duplicate schedule exceptions on " + date.toString());

                                service.calendar_dates.put(date, calendarDate);
                                calendarDateCount += 1;
                            }
                        }
                        feed.services.put(cal.service_id, service);
                    }
                    if (calendarDateCount == 0) {
                        LOG.info("No calendar dates found. Skipping table.");
                    } else {
                        LOG.info("Writing {} calendar dates from schedule exceptions", calendarDateCount);
                        new CalendarDate.Writer(feed).writeTable(zipOutputStream);
                    }
                } else {
                    // No calendar records exist, export calendar_dates as is and hope for the best.
                    // This situation will occur in at least 2 scenarios:
                    // 1.  A GTFS has been loaded into the editor that had only the calendar_dates.txt file
                    //     and no further edits were made before exporting to a snapshot
                    // 2.  A new GTFS has been created from scratch and calendar information has yet to be added.
                    //     This will result in an invalid GTFS, but it was what the user wanted so ¯\_(ツ)_/¯
                    result.calendarDates = export(Table.CALENDAR_DATES, connection);
                }
            } else {
                // Otherwise, simply export the calendar dates as they were loaded in.
                result.calendarDates = export(Table.CALENDAR_DATES, connection);
            }
            result.fareAttributes = export(Table.FARE_ATTRIBUTES, connection);
            result.fareRules = export(Table.FARE_RULES, connection);
            result.feedInfo = export(Table.FEED_INFO, connection);
            // Only write frequencies for "approved" routes using COPY TO with results of select query
            if (fromEditor) {
                // Generate filter SQL for trips if exporting a feed/schema that represents an editor snapshot.
                // The filter clause for frequencies requires two joins to reach the routes table and a where filter on
                // route status.
                // FIXME Replace with string literal query instead of clause generators
                result.frequencies = export(
                    Table.FREQUENCIES,
                    String.join(
                        " ",
                        // Convert start_time and end_time values from seconds to time format (HH:MM:SS).
                        Table.FREQUENCIES.generateSelectSql(feedIdToExport, Requirement.OPTIONAL),
                        Table.FREQUENCIES.generateJoinSql(Table.TRIPS, feedIdToExport),
                        Table.TRIPS.generateJoinSql(
                            Table.ROUTES,
                            feedIdToExport,
                            "route_id",
                            false
                        ),
                        whereRouteIsApproved
                    )
                );
            } else {
                result.frequencies = export(Table.FREQUENCIES, connection);
            }

            // Only write "approved" routes using COPY TO with results of select query
            if (fromEditor) {
                // The filter clause for routes is simple. We're just checking that the route is APPROVED.
                result.routes = export(
                    Table.ROUTES,
                    String.join(
                        " ",
                        Table.ROUTES.generateSelectSql(feedIdToExport, Requirement.OPTIONAL),
                        whereRouteIsApproved
                    )
                );
            } else {
                result.routes = export(Table.ROUTES, connection);
            }

            // FIXME: Find some place to store errors encountered on export for patterns and pattern stops.
            // FIXME: Is there a need to export patterns or pattern stops? Should these be iterated over to ensure that
            // frequency-based pattern travel times match stop time arrivals/departures?
//            export(Table.PATTERNS);
//            export(Table.PATTERN_STOP);
            // Only write shapes for "approved" routes using COPY TO with results of select query
            if (fromEditor) {
                // Generate filter SQL for shapes if exporting a feed/schema that represents an editor snapshot.
                // The filter clause for shapes requires joining to trips and then to routes table and a where filter on
                // route status.
                // FIXME: I'm not sure that shape_id is indexed for the trips table. This could cause slow downs.
                // FIXME: this is exporting point_type, which is not a GTFS field, but its presence shouldn't hurt.
                String shapeFieldsToExport = Table.commaSeparatedNames(
                    Table.SHAPES.specFields(),
                    String.join(".", feedIdToExport, Table.SHAPES.name + "."),
                    true
                );
                // NOTE: The below substitution uses relative indexing. All values "%<s" reference the same arg as the
                // previous format specifier (i.e., feedIdToExport).
                result.shapes = export(
                    Table.SHAPES,
                    String.format(
                        "select %s " +
                            "from (select distinct %s.trips.shape_id " +
                            "from %<s.trips, %<s.routes " +
                            "where %<s.trips.route_id = %<s.routes.route_id and " +
                            "%<s.routes.status = 2) as unique_approved_shape_ids, " +
                            "%<s.shapes where unique_approved_shape_ids.shape_id = %<s.shapes.shape_id",
                        shapeFieldsToExport,
                        feedIdToExport
                    )
                );
            } else {
                result.shapes = export(Table.SHAPES, connection);
            }
            result.stops = export(Table.STOPS, connection);
            // Only write stop times for "approved" routes using COPY TO with results of select query
            if (fromEditor) {
                // Generate filter SQL for trips if exporting a feed/schema that represents an editor snapshot.
                // The filter clause for stop times requires two joins to reach the routes table and a where filter on
                // route status.
                // FIXME Replace with string literal query instead of clause generators
                result.stopTimes = export(
                    Table.STOP_TIMES,
                    String.join(" ",
                        Table.STOP_TIMES.generateSelectSql(feedIdToExport, Requirement.OPTIONAL),
                        Table.STOP_TIMES.generateJoinSql(Table.TRIPS, feedIdToExport),
                        Table.TRIPS.generateJoinSql(
                            Table.ROUTES,
                            feedIdToExport,
                            "route_id",
                            false
                        ),
                        whereRouteIsApproved
                    )
                );
            } else {
                result.stopTimes = export(Table.STOP_TIMES, connection);
            }
            result.transfers = export(Table.TRANSFERS, connection);
            if (fromEditor) {
                // Generate filter SQL for trips if exporting a feed/schema that represents an editor snapshot.
                // The filter clause for trips requires an inner join on the routes table and the same where check on
                // route status.
                // FIXME Replace with string literal query instead of clause generators
                result.trips = export(
                    Table.TRIPS,
                    String.join(" ",
                        Table.TRIPS.generateSelectSql(feedIdToExport, Requirement.OPTIONAL),
                        Table.TRIPS.generateJoinSql(
                            Table.ROUTES,
                            feedIdToExport,
                            "route_id",
                            false
                        ),
                        whereRouteIsApproved
                    )
                );
            } else {
                result.trips = export(Table.TRIPS, connection);
            }

            zipOutputStream.close();
            // Run clean up on the resulting zip file.
            cleanUpZipFile();
            result.completionTime = System.currentTimeMillis();
            result.loadTimeMillis = result.completionTime - startTime;
            // Exporting primary GTFS tables for GRTA Xpress = 12 sec
            LOG.info("Exporting tables took {} sec", (result.loadTimeMillis) / 1000);
            LOG.info("Exported feed {} to zip file: {}", feedIdToExport, outFile);
        } catch (Exception ex) {
            // Note: Exceptions that occur during individual table loads are separately caught and stored in
            // TableLoadResult.
            LOG.error("Exception while creating snapshot: {}", ex.toString());
            ex.printStackTrace();
            result.fatalException = ex.toString();
        }
        return result;
    }
public FeedLoadResult exportTables ( ) { // This result object will be returned to the caller to summarize the feed and report any critical errors. // FIXME: use feed export result object? FeedLoadResult result = new FeedLoadResult ( ) ; try { zipOutputStream = new ZipOutputStream ( new FileOutputStream ( outFile ) ) ; long startTime = System . currentTimeMillis ( ) ; // We get a single connection object and share it across several different methods. // This ensures that actions taken in one method are visible to all subsequent SQL statements. // If we create a schema or table on one connection, then access it in a separate connection, we have no // guarantee that it exists when the accessing statement is executed. connection = dataSource . getConnection ( ) ; if ( ! connection . getMetaData ( ) . getDatabaseProductName ( ) . equals ( "PostgreSQL" ) ) { // This code path currently requires the Postgres text "copy to" format. throw new RuntimeException ( "Export from SQL database not implemented for non-PostgreSQL databases." ) ; } // Construct where clause for routes table to filter only "approved" routes (and entities related to routes) // if exporting a feed/schema that represents an editor snapshot. String whereRouteIsApproved = String . format ( "where %s.%s.status = 2" , feedIdToExport , Table . ROUTES . name ) ; // Export each table in turn (by placing entry in zip output stream). result . agency = export ( Table . AGENCY , connection ) ; if ( fromEditor ) { // only export calendar entries that have at least one day of service set // this could happen in cases where a feed was imported that only had calendar_dates.txt result . calendar = export ( Table . CALENDAR , String . join ( " " , Table . CALENDAR . generateSelectSql ( feedIdToExport , Requirement . OPTIONAL ) , "WHERE monday=1 OR tuesday=1 OR wednesday=1 OR thursday=1 OR friday=1 OR saturday=1 OR sunday=1" ) ) ; } else { result . calendar = export ( Table . CALENDAR , connection ) ; } if ( fromEditor ) { // Export schedule exceptions in place of calendar dates if exporting a feed/schema that represents an editor snapshot. GTFSFeed feed = new GTFSFeed ( ) ; // FIXME: The below table readers should probably just share a connection with the exporter. JDBCTableReader < ScheduleException > exceptionsReader = new JDBCTableReader ( Table . SCHEDULE_EXCEPTIONS , dataSource , feedIdToExport + "." , EntityPopulator . SCHEDULE_EXCEPTION ) ; JDBCTableReader < Calendar > calendarsReader = new JDBCTableReader ( Table . CALENDAR , dataSource , feedIdToExport + "." , EntityPopulator . CALENDAR ) ; Iterable < Calendar > calendars = calendarsReader . getAll ( ) ; Iterable < ScheduleException > exceptionsIterator = exceptionsReader . getAll ( ) ; List < ScheduleException > exceptions = new ArrayList <> ( ) ; // FIXME: Doing this causes the connection to stay open, but it is closed in the finalizer so it should // not be a big problem. for ( ScheduleException exception : exceptionsIterator ) { exceptions . add ( exception ) ; } // check whether the feed is organized in a format with the calendars.txt file if ( calendarsReader . getRowCount ( ) > 0 ) { // feed does have calendars.txt file, continue export with strategy of matching exceptions // to calendar to output calendar_dates.txt int calendarDateCount = 0 ; for ( Calendar cal : calendars ) { Service service = new Service ( cal . service_id ) ; service . calendar = cal ; for ( ScheduleException ex : exceptions ) { if ( ex . exemplar . equals ( ScheduleException . ExemplarServiceDescriptor . SWAP ) && ( ! ex . addedService . contains ( cal . service_id ) && ! ex . removedService . contains ( cal . service_id ) ) ) { // Skip swap exception if cal is not referenced by added or removed service. // This is not technically necessary, but the output is cleaner/more intelligible. continue ; } for ( LocalDate date : ex . dates ) { if ( date . isBefore ( cal . start_date ) || date . isAfter ( cal . end_date ) ) { // No need to write dates that do not apply continue ; } CalendarDate calendarDate = new CalendarDate ( ) ; calendarDate . date = date ; calendarDate . service_id = cal . service_id ; calendarDate . exception_type = ex . serviceRunsOn ( cal ) ? 1 : 2 ; LOG . info ( "Adding exception {} (type={}) for calendar {} on date {}" , ex . name , calendarDate . exception_type , cal . service_id , date . toString ( ) ) ; if ( service . calendar_dates . containsKey ( date ) ) throw new IllegalArgumentException ( "Duplicate schedule exceptions on " + date . toString ( ) ) ; service . calendar_dates . put ( date , calendarDate ) ; calendarDateCount += 1 ; } } feed . services . put ( cal . service_id , service ) ; } if ( calendarDateCount == 0 ) { LOG . info ( "No calendar dates found. Skipping table." ) ; } else { LOG . info ( "Writing {} calendar dates from schedule exceptions" , calendarDateCount ) ; new CalendarDate . Writer ( feed ) . writeTable ( zipOutputStream ) ; } } else { // No calendar records exist, export calendar_dates as is and hope for the best. // This situation will occur in at least 2 scenarios: // 1.  A GTFS has been loaded into the editor that had only the calendar_dates.txt file //     and no further edits were made before exporting to a snapshot // 2.  A new GTFS has been created from scratch and calendar information has yet to be added. //     This will result in an invalid GTFS, but it was what the user wanted so ¯\_(ツ)_/¯ result . calendarDates = export ( Table . CALENDAR_DATES , connection ) ; } } else { // Otherwise, simply export the calendar dates as they were loaded in. result . calendarDates = export ( Table . CALENDAR_DATES , connection ) ; } result . fareAttributes = export ( Table . FARE_ATTRIBUTES , connection ) ; result . fareRules = export ( Table . FARE_RULES , connection ) ; result . feedInfo = export ( Table . FEED_INFO , connection ) ; // Only write frequencies for "approved" routes using COPY TO with results of select query if ( fromEditor ) { // Generate filter SQL for trips if exporting a feed/schema that represents an editor snapshot. // The filter clause for frequencies requires two joins to reach the routes table and a where filter on // route status. // FIXME Replace with string literal query instead of clause generators result . frequencies = export ( Table . FREQUENCIES , String . join ( " " , // Convert start_time and end_time values from seconds to time format (HH:MM:SS). Table . FREQUENCIES . generateSelectSql ( feedIdToExport , Requirement . OPTIONAL ) , Table . FREQUENCIES . generateJoinSql ( Table . TRIPS , feedIdToExport ) , Table . TRIPS . generateJoinSql ( Table . ROUTES , feedIdToExport , "route_id" , false ) , whereRouteIsApproved ) ) ; } else { result . frequencies = export ( Table . FREQUENCIES , connection ) ; } // Only write "approved" routes using COPY TO with results of select query if ( fromEditor ) { // The filter clause for routes is simple. We're just checking that the route is APPROVED. result . routes = export ( Table . ROUTES , String . join ( " " , Table . ROUTES . generateSelectSql ( feedIdToExport , Requirement . OPTIONAL ) , whereRouteIsApproved ) ) ; } else { result . routes = export ( Table . ROUTES , connection ) ; } // FIXME: Find some place to store errors encountered on export for patterns and pattern stops. // FIXME: Is there a need to export patterns or pattern stops? Should these be iterated over to ensure that // frequency-based pattern travel times match stop time arrivals/departures? //            export(Table.PATTERNS); //            export(Table.PATTERN_STOP); // Only write shapes for "approved" routes using COPY TO with results of select query if ( fromEditor ) { // Generate filter SQL for shapes if exporting a feed/schema that represents an editor snapshot. // The filter clause for shapes requires joining to trips and then to routes table and a where filter on // route status. // FIXME: I'm not sure that shape_id is indexed for the trips table. This could cause slow downs. // FIXME: this is exporting point_type, which is not a GTFS field, but its presence shouldn't hurt. String shapeFieldsToExport = Table . commaSeparatedNames ( Table . SHAPES . specFields ( ) , String . join ( "." , feedIdToExport , Table . SHAPES . name + "." ) , true ) ; // NOTE: The below substitution uses relative indexing. All values "%<s" reference the same arg as the // previous format specifier (i.e., feedIdToExport). result . shapes = export ( Table . SHAPES , String . format ( "select %s " + "from (select distinct %s.trips.shape_id " + "from %<s.trips, %<s.routes " + "where %<s.trips.route_id = %<s.routes.route_id and " + "%<s.routes.status = 2) as unique_approved_shape_ids, " + "%<s.shapes where unique_approved_shape_ids.shape_id = %<s.shapes.shape_id" , shapeFieldsToExport , feedIdToExport ) ) ; } else { result . shapes = export ( Table . SHAPES , connection ) ; } result . stops = export ( Table . STOPS , connection ) ; // Only write stop times for "approved" routes using COPY TO with results of select query if ( fromEditor ) { // Generate filter SQL for trips if exporting a feed/schema that represents an editor snapshot. // The filter clause for stop times requires two joins to reach the routes table and a where filter on // route status. // FIXME Replace with string literal query instead of clause generators result . stopTimes = export ( Table . STOP_TIMES , String . join ( " " , Table . STOP_TIMES . generateSelectSql ( feedIdToExport , Requirement . OPTIONAL ) , Table . STOP_TIMES . generateJoinSql ( Table . TRIPS , feedIdToExport ) , Table . TRIPS . generateJoinSql ( Table . ROUTES , feedIdToExport , "route_id" , false ) , whereRouteIsApproved ) ) ; } else { result . stopTimes = export ( Table . STOP_TIMES , connection ) ; } result . transfers = export ( Table . TRANSFERS , connection ) ; if ( fromEditor ) { // Generate filter SQL for trips if exporting a feed/schema that represents an editor snapshot. // The filter clause for trips requires an inner join on the routes table and the same where check on // route status. // FIXME Replace with string literal query instead of clause generators result . trips = export ( Table . TRIPS , String . join ( " " , Table . TRIPS . generateSelectSql ( feedIdToExport , Requirement . OPTIONAL ) , Table . TRIPS . generateJoinSql ( Table . ROUTES , feedIdToExport , "route_id" , false ) , whereRouteIsApproved ) ) ; } else { result . trips = export ( Table . TRIPS , connection ) ; } zipOutputStream . close ( ) ; // Run clean up on the resulting zip file. cleanUpZipFile ( ) ; result . completionTime = System . currentTimeMillis ( ) ; result . loadTimeMillis = result . completionTime - startTime ; // Exporting primary GTFS tables for GRTA Xpress = 12 sec LOG . info ( "Exporting tables took {} sec" , ( result . loadTimeMillis ) / 1000 ) ; LOG . info ( "Exported feed {} to zip file: {}" , feedIdToExport , outFile ) ; } catch ( Exception ex ) { // Note: Exceptions that occur during individual table loads are separately caught and stored in // TableLoadResult. LOG . error ( "Exception while creating snapshot: {}" , ex . toString ( ) ) ; ex . printStackTrace ( ) ; result . fatalException = ex . toString ( ) ; } return result ; }
Export primary entity tables as well as Pattern and PatternStops tables.

FIXME: This should probably only export main GTFS tables... unless there is a benefit to keeping editor fields or
tables intact for future editing. Maybe this could be an option provided to the function (e.g., specTablesOnly).
Export primary entity tables as well as Pattern and PatternStops tables .