google / lovefield

Lovefield is a relational database for web apps. Written in JavaScript, works cross-browser. Provides SQL-like APIs that are fast, safe, and easy to use.
https://google.github.io/lovefield/
Apache License 2.0
6.82k stars 366 forks source link

Constraint error: (201) Duplicate keys are not allowed #172

Closed bimusiek closed 7 years ago

bimusiek commented 8 years ago

Hey, I am using lovefield and from some time to time I am getting this exception:

Constraint error: (201) Duplicate keys are not allowed, index: FlightBookingLeg.pkFlightBookingLeg, key: 2560

However, all around app I am using insertOrReplace so there is no way I am adding some duplicated keys. You can see that primary key is duplicated somehow, but as I understand insertOrReplace it should ensure that it won't duplicate anything right?

I am using WebSQL because IndexedDB does not work in Safari. Does anyone know what can cause it or how to prevent it?

Screenshot from Chrome dev tools, you can see there are 2 duplicated keys with value 2560:

screenshot 2016-08-10 18 32 16

Just to explain, I am enforcing that connection happens only once:

const db = require("imports?this=>window!exports?ahoydb&lf!./ahoydb");

class Database {
  constructor () {
    this.ahoydb = db.ahoydb;
    this.lf = db.lf;
    this.connection = null;
    this.isConnecting = false;
    this.connectionObservers = [];
  }

  connect () {
    if (this.connection) {
      return Promise.resolve(this);
    }

    if (this.isConnecting) {
      return new Promise((resolve) => {
        this.connectionObservers.push(resolve);
      });
    }
    this.isConnecting = true;
    return db.ahoydb.connect({
      onUpgrade: this.onUpgrade.bind(this),
      storeType: this.lf.schema.DataStoreType.WEB_SQL
    }).then((db) => {
      this.connected(db);
      return this;
    });
  }

  connected (db) {
    this.connection = db;
    this.isConnecting = false;
    this.connectionObservers.forEach((resolve) => {
      resolve(this);
    });
    this.connectionObservers = [];
  }

  onUpgrade (rawDb) {
    const version = rawDb.getVersion();

    let promises = [];
    if (version < 2) {
      promises.push(rawDb.addTableColumn(", ", 0));
    }
    if (version < 3) {
      promises.push(rawDb.addTableColumn(", ", 0));
    }
    if (version < 4) {
      promises.push(rawDb.addTableColumn(", ", "0"));
    }
    return Promise.all(promises);
  }
}

export default new Database();
freshp86 commented 8 years ago

How does your schema look like? The fact that you are using insertOrReplace() does not guarantee that there will be no collisions of PK. You can still be accidentally attempting to insert the same PK twice, since you can supply your own primary keys.

Are you using the "auto-increment primary key" feature? Could you post some snippets of the code that inserts to the DB?

Also a couple unrelated suggestions.

  1. Suggesting to only use WEB_SQL in Safari, instead of using it everywhere to accommodate Safari. In fact, if you don't specify a storeType, Lovefield will automatically fall back to WEB_SQL on Safari, and use INDEXED_DB otherwise, see fallback logic at https://github.com/google/lovefield/blob/master/lib/base.js#L58-L64.
  2. The code can be simplified a bit. There is no need for connectionObservers variable, or for the isConnecting boolean.

Instead you could simply store a single Promise, and return that promise from future calls to connect().

 connect () {
    if (this.whenConnected) {
      return this.whenConnected;
    }

    this.whenConnected = db.ahoydb.connect({
      onUpgrade: this.onUpgrade.bind(this),
      storeType: this.lf.schema.DataStoreType.WEB_SQL
    }).then((db) => {
      this.connection = db;
      return this;
    });
   return this.whenConnected;
  }
freshp86 commented 8 years ago

Actually let me rephrase one of my comments above.

The fact that you are using insertOrReplace() means that if you insert a row with the same PK, it should replace an existing row instead of complaining. So, yes, that would be a bug if it is not happening. Again, please post an example of the schema and the code that inserts to the DB, to see if anything stands out.

bimusiek commented 8 years ago

I am using SPAC to generate schema files, so my schema (yml) looks like this:

%YAML 1.2
---
name: ahoydb
version: 7
table:
  FlightBookingGrouper:
    column:
      id: integer
    constraint:
      primaryKey: [ id ]

  FlightBooking:
    column:
      id: integer
      reference: string
      currency: string
      modifiedAt: datetime
      createdAt: datetime
      status: string
      grouperId: integer
      airlineId: integer
      amount: string
    constraint:
      primaryKey: [ id ]
      foreignKey:
        fkGrouperId:
          local: grouperId
          ref: FlightBookingGrouper.id
        fkAirlineId:
          local: airlineId
          ref: Airline.id

  FlightBookingLeg:
    column:
      id: integer
      flightBookingId: integer
      firstSectionTakeoffDateTZ: datetime
      lastSectionLandingDateTZ: datetime
    constraint:
      primaryKey: [ id ]
      foreignKey:
        fkFlightBookingId:
          local: flightBookingId
          ref: FlightBooking.id
          action: cascade

  FlightBookingSection:
    column:
      id: integer
      legId: integer
      flightBookingId: integer
      flightNumber: string
      seat: string
      gate: string
      terminal: string
      fareType: string
      baggageAllowance: string
      flightDuration: number
      takeoffDate: datetime
      landingDate: datetime
      takeoffDateTZ: datetime
      landingDateTZ: datetime
      boardingDate: datetime
      ticketCancellationFee: boolean
      ticketCancellationFeeCurrency: string
      ticketCancellationFeeAmount: string
      ticketChangeFee: boolean
      ticketChangeFeeCurrency: string
      ticketChangeFeeAmount: string
      status: string
      airlineId: integer
      arrivalAirportId: integer
      departureAirportId: integer
    constraint:
      nullable: [seat, gate, terminal, fareType, flightNumber, baggageAllowance, ticketCancellationFeeCurrency, ticketCancellationFeeAmount, ticketChangeFeeCurrency, ticketChangeFeeAmount, boardingDate]
      primaryKey: [ id ]
      fkLegId:
        local: legId
        ref: FlightBookingLeg.id
      fkFlightBookingId:
        local: flightBookingId
        ref: FlightBooking.id
        action: cascade
      fkAirlineId:
        local: airlineId
        ref: Airline.id
      fkArrivalAirportId:
        local: arrivalAirportId
        ref: Airport.id
      fkDepartureAirportId:
        local: departureAirportId
        ref: Airport.id

  Airline:
    column:
      id: integer
      name: string
    constraint:
      primaryKey: [ id ]

  Airport:
    column:
      id: integer
      name: string
      city: string
      iata: string
      tz: string
      dst: string
      latitude: string
      longitude: string
    constraint:
      primaryKey: [ id ]

  Passenger:
    column:
      id: integer
      ticketNumber: string
      flightBookingId: integer
      ticketPrice: string
      taxAmount: string
      customerId: integer
    constraint:
      primaryKey: [ id ]
      nullable: [ticketNumber, ticketPrice, taxAmount]
      fkFlightBookingId:
        local: flightBookingId
        ref: FlightBooking.id
        action: cascade
      fkCustomerId:
        local: customerId
        ref: Customer.id

  Customer:
    column:
      id: integer
      title: string
      firstName: string
      middleName: string
      lastName: string
      gender: string
      smoochId: string
      nationality: string
      company: string
      notes: string
      travelPreferences: string
      birthday: datetime
    constraint:
      primaryKey: [id]
      nullable: [title, firstName, middleName, lastName, gender, smoochId, nationality, company, notes, travelPreferences, birthday]

  Charge:
    column:
      id: integer
      amount: string
      type: string
      name: string
      flightBookingId: integer
    constraint:
      primaryKey: [id]
      nullable: [name]
      fkFlightBookingId:
        local: flightBookingId
        ref: FlightBooking.id
        action: cascade

  ChargeRequest:
    column:
      id: integer
      expireAt: datetime
      flightBookingId: integer
      type: string
      amount: string
      currency: string
    constraint:
      primaryKey: [id]
      nullable: [type]
      fkFlightBookingId:
        local: flightBookingId
        ref: FlightBooking.id
        action: cascade

  ChargeRequestAck:
    column:
      id: integer
      customerId: integer
      statusChangedAt: datetime
      status: string
      creditCardId: integer
      forwardToUserId: integer
      chargeRequestId: integer
    constraint:
      primaryKey: [id]
      nullable: [creditCardId, forwardToUserId, statusChangedAt]
      fkForwardToUserId:
        local: forwardToUserId
        ref: User.id
      fkCreditCardId:
        local: creditCardId
        ref: Card.id
      fkChargeRequestId:
        local: chargeRequestId
        ref: ChargeRequest.id
        action: cascade

  Card:
    column:
      id: integer
      name: string
      mask: string
      brand: string
      addressName: string
      addressLine1: string
      addressLine2: string
      addressCity: string
      addressZip: string
      addressState: string
      addressCountry: string
    constraint:
      primaryKey: [id]
      nullable: [addressName, addressLine1, addressLine2, addressCity, addressZip, addressState, addressCountry]

  User:
    column:
      id: integer
      username: string
      customerId: integer
    constraint:
      primaryKey: [id]
      fkCustomerId:
        local: customerId
        ref: Customer.id

Then this is how I create Leg model for example

static map (db, legId, sections, bookingId) {
    const FlightBookingLeg = db.connection.getSchema().getFlightBookingLeg();
    const row = FlightBookingLeg.createRow({
      id: parseInt(legId),
      flightBookingId: bookingId,
      firstSectionTakeoffDateTZ: sections[ 0 ].takeoffDate.toDate(),
      lastSectionLandingDateTZ: sections[ sections.length - 1 ].landingDate.toDate()
    });

    return db.connection.insertOrReplace().into(FlightBookingLeg).values([ row ]).exec().then(
      (rows) => {
        return rows[ 0 ];
      })
      .then((leg) => {
        const promises = sections.map((section) => {
          return FlightSectionMapper.map(db, section, legId, bookingId);
        });
        return Promise.all(promises).then(() => {
          return leg;
        });
      });
  }

About your unrelated suggestions:

  1. I thought so, however, while testing behaviour on iOS 9 and iOS 10, the database was not persistent. It loaded the first time and it worked, but then when refreshing page data disappeared. I already commented about this in another issue :)
  2. You are right, thanks for the suggestion.
bimusiek commented 8 years ago

Also, I am genereting lovefield sources quite unusual. Because SPAC generates file with adding Google Closure stuff to the code, I had to checkout latest lovefield and google closure library and I am using compiler.jar to generate one file with my schema + lovefield.

Code generation looks like this:

${SCRIPTPATH}/spac.sh --schema ${SCRIPTPATH}/../app/libs/database/schema.yml --outputdir ${SCRIPTPATH}/../app/libs/database/lovefield/ --namespace ahoydb
java -jar ${SCRIPTPATH}/../tools/lovefield/node_modules/google-closure-compiler/compiler.jar --dependency_mode=STRICT --entry_point='ahoydb.row.FlightBooking' --js_output_file=${SCRIPTPATH}/../app/libs/database/ahoydb.js "${SCRIPTPATH}/../tools/lovefield/node_modules/google-closure-library/**.js" '!**_test.js' "${SCRIPTPATH}/../tools/lovefield/lib/**.js" "${SCRIPTPATH}/../app/libs/database/lovefield/**.js"

I couldn't make it work otherwise with Webpack, this way I get one file with ahoydb and lf variables that webpack can export from the file and I can access them outside.

bimusiek commented 8 years ago

I tried to investigate more, however, I couldn't find why this happens. Exception is thrown already after Row was inserted so I have no idea even when duplicated Row is inserted.

I am using all around the code only insertOrReplace, no updates, no inserts etc.

My workaround, for now, is to in case of error, change DB name (save it to local storage) and then I am overriding ahoydb.schema.Database.prototype.name. This way database is recreated.

freshp86 commented 8 years ago

Thanks for the snippets. I could not spot anything suspicious in the schema. Also the way you are building Lovefield, given that you are using SPAC seems fine too.

It is hard to remote debug the issue. A minimal repro case would be helpful, but I guess you don't have an easy minimal repro yet. I suggest the following:

  1. Compile Lovefield + the SPAC generated files in debug mode such that it can be human readable.
  2. Add a breakpoint at https://github.com/google/lovefield/blob/master/lib/cache/journal.js#L259. This is where the insert VS replace is determined, and the constraint checks are chose based on that (see calls to checkForeignKeysForUpdate or checkForeignKeysForUpdate).

From the symptoms you are describing it seems that Lovefield is mis-classifying a "replace" for an "insert" and therefore it performs the wrong constraint checks resulting in the error above.

From the screenshot you posted at the 1st post, the Database is already in a corrupt state (duplicate PK has been persisted), so from that point on, every time you reload the page, an error will be thrown. The question is to figure out how was the DB corrupted and fix that.

Other things to try: Can you reproduce this with INDEXED_DB? Perhaps it could be a bug affecting WEB_SQL only.

Regarding unrelated suggestion 1 in previous post: Yes this is a known issue 168. I am suggesting to do your own IOS detection until it is fixed, instead of using WEB_SQL everywhere, such that you always specify the "storeType" in connectOptions.

bimusiek commented 8 years ago

I did not have time to debug with unminified code, however I changed detection so WebSQL is used only on Safari and all looks fine so far. However my hack could also help where in case of database problem I am dynamicly changing the name (and persisting in local storage) so db will be recreated.

I did not find easy way to re-create database so I had to do this trick with making new one with different name.

nccnm commented 8 years ago

I have the same issue with IndexedDB storage. I open my application in 2 tabs in Chrome browser. If a tab deletes a record in IndexedDB but the remain tab still adds this record with same id to IndexedDB, we will have 2 record in IndexedDB with the same primary key. I close the browser and open again, the 201 error will say 'hi' to me.

bimusiek commented 8 years ago

@nccnm Awesome that you found why it fails! I only saw results of the bug.

If you are interested here is how I fixed it, I know it is not pretty or perfect but it works 👍

import SynchronizerDispatcher from "../../synchronizer/dispatcher";
import Console from "../console";
const db = require("imports?this=>window!exports?ahoydb&lf!./ahoydb");

const DATABASE_NAME_LS = "lovefield.database.name";

db.ahoydb.schema.Database.prototype.name = () => {
  let lsDbName = localStorage.getItem(DATABASE_NAME_LS);
  if(!lsDbName) {
    lsDbName = "ahoydb_web";
    localStorage.setItem(DATABASE_NAME_LS, lsDbName);
  }
  return lsDbName;
};

class Database {
  constructor () {
    this.ahoydb = db.ahoydb;
    this.lf = db.lf;
    this.connection = null;
    this.whenConnected = null;
  }

  connect () {
    if (this.whenConnected) {
      return this.whenConnected;
    }
    const self = this;

    var props = {
      onUpgrade: this.onUpgrade.bind(this)
    };

    if (navigator.userAgent.indexOf("Safari") != -1 && navigator.userAgent.indexOf("Chrome") == -1) {
      props.storeType = this.lf.schema.DataStoreType.WEB_SQL;
    }
    this.whenConnected = db.ahoydb.connect(props).then((db) => {
      self.connection = db;
      return this;
    }, error => {
      if(error.code == 201 || error.code == 5) {
        return self.resolveDbError();
      }
    });
    return this.whenConnected;
  }

  onUpgrade (rawDb) {
    const version = rawDb.getVersion();
    Console.log("Upgrading ver. "+version);

    return Promise.resolve().then(() => {
      if(version < 2) {
        return rawDb.addTableColumn("ChargeRequest", "extensionCounter", 0);
      }
    }).then(() => {
      if(version < 3) {
        return rawDb.addTableColumn("FlightBookingGrouper", "lastFlightBookingId", 0);
      }
    }).then(() => {
      // have it as the last element of the promise chain
      return rawDb.dump();
    });

  }

  resolveDbError() {
    // One way to fix database is to rename it so it will be re-created
    localStorage.setItem(DATABASE_NAME_LS, localStorage.getItem(DATABASE_NAME_LS)+"_");
    this.whenConnected = null;
    SynchronizerDispatcher.clear();
    return this.connect();
  }
}

export default new Database();

Unfortunately I did not find the easy way to delete and recreate the database, because when you get error LF is already connected and not all the browsers offer the option to really remove the db. Another temporary solution would be to clean the fields that are failing however again you cannot do it with LF as it just failed and you would have to connect directly to the database by yourself, which makes the whole LF obsolete then for me...

freshp86 commented 8 years ago

/cc @arthurhsu Multiple tabs are not supported, per https://github.com/google/lovefield/blob/master/docs/spec/99_postfix.md#multi-connection. There is currently no (easy) way to sync the state (in-memory indices, in-memory cache) across the two tabs, therefore the DB gets corrupted (constraint violations make it to the persisted data) when used that way.

freshp86 commented 8 years ago

See also https://github.com/google/lovefield/blob/master/docs/spec/03_life_of_db.md#32-multi-process-connection.

veenedu commented 7 years ago

@bimusiek Hi, Could you explain a bit how you generated Schema file from YML file. Thanks.

bimusiek commented 7 years ago

@veenedu Script to install lovefield localy:

#!/usr/bin/env bash

# Install Lovefield SPAC

OLD_PWD=$PWD
mkdir tools | true
cd tools
rm -rf lovefield | true
git clone https://github.com/google/lovefield.git
cd lovefield
git checkout v2.1.11
npm install
cd $OLD_PWD

and then I have spac.sh helper

#!/usr/bin/env bash
SCRIPTPATH=$(dirname $BASH_SOURCE)
exec node $SCRIPTPATH/lovefield/spac/spac.js $@

and finally script to generate lovefield with schema

#!/usr/bin/env bash
SCRIPTPATH=$(dirname $BASH_SOURCE)
${SCRIPTPATH}/spac.sh --schema ${SCRIPTPATH}/../web/app/libs/database/schema.yml --outputdir ${SCRIPTPATH}/../web/app/libs/database/lovefield/ --namespace ahoydb
java -jar ${SCRIPTPATH}/../tools/lovefield/node_modules/google-closure-compiler/compiler.jar --dependency_mode=STRICT --entry_point='ahoydb.row.FlightBooking' --js_output_file=${SCRIPTPATH}/../web/app/libs/database/ahoydb.js "${SCRIPTPATH}/../tools/lovefield/node_modules/google-closure-library/**.js" '!**_test.js' "${SCRIPTPATH}/../tools/lovefield/lib/**.js" "${SCRIPTPATH}/../web/app/libs/database/lovefield/**.js"

You have to tweak it for your purposes, so you have to change entry_point and js_output_file and thats should be all. :)

veenedu commented 7 years ago

@bimusiek

for below it generates YAML it generates Javascript file, as specified far below. That file contains lots of goog.provide at the beginning. Is that expected behaviour? Do you think I have generated correct schema file? If yes how to resolve those goog

Yam File

%YAML 1.2
---
name: crdb
version: 1
table:
  ImageCache:
    column:
      remote: string
      local: string
    constraint:
      primaryKey: [ remote ]

  Asset:
    column:
      id: string
      asset: string
      timestamp: integer
    constraint:
      primaryKey: [ id ]

  Pin:
    column:
      id: string
      state: integer
      sessionId: string
    constraint:
      foreignKey:
        fkId:
          local: id
          ref: Asset.id
          action: cascade

  InfoCard:
    column:
      id: string
      lang: string
      itag: integer
      country: string
      fileName: string
    constraint:
      primaryKey: [ id, lang ]
      unique:
        uniqFN:
          column: [ fileName ]
    index:
      idxPinItag:
        column: [ itag ]

Output File

goog.provide('veen.row.Asset');
goog.provide('veen.row.AssetDbType');
goog.provide('veen.row.AssetType');
goog.provide('veen.row.ImageCache');
goog.provide('veen.row.ImageCacheDbType');
goog.provide('veen.row.ImageCacheType');
goog.provide('veen.row.InfoCard');
goog.provide('veen.row.InfoCardDbType');
goog.provide('veen.row.InfoCardType');
goog.provide('veen.row.Pin');
goog.provide('veen.row.PinDbType');
goog.provide('veen.row.PinType');
goog.provide('veen.schema.Asset');
goog.provide('veen.schema.Database');
goog.provide('veen.schema.ImageCache');
goog.provide('veen.schema.InfoCard');
goog.provide('veen.schema.Pin');

/** @suppress {extraRequire} */
goog.require('lf.ConstraintAction');
goog.require('lf.ConstraintTiming');
goog.require('lf.Order');
goog.require('lf.Row');
goog.require('lf.Type');
goog.require('lf.schema.BaseColumn');
goog.require('lf.schema.Constraint');
goog.require('lf.schema.Database');
goog.require('lf.schema.ForeignKeySpec');
goog.require('lf.schema.Index');
goog.require('lf.schema.Info');
goog.require('lf.schema.Table');

/**
 * @implements {lf.schema.Database}
 * @constructor
 */
veen.schema.Database = function() {
  /** @private {!Object} */
  this.tableMap_ = {};

  /** @private {!lf.schema.Database.Pragma} */
  this.pragma_ = {
    enableBundledMode: false
  };

  /** @private {!veen.schema.ImageCache} */
  this.imageCache_ = new veen.schema.ImageCache();
  this.tableMap_['ImageCache'] = this.imageCache_;

  /** @private {!veen.schema.Asset} */
  this.asset_ = new veen.schema.Asset();
  this.tableMap_['Asset'] = this.asset_;

  /** @private {!veen.schema.Pin} */
  this.pin_ = new veen.schema.Pin();
  this.tableMap_['Pin'] = this.pin_;

  /** @private {!veen.schema.InfoCard} */
  this.infoCard_ = new veen.schema.InfoCard();
  this.tableMap_['InfoCard'] = this.infoCard_;

  /** @private {!lf.schema.Info} */
  this.metaInfo_;
};

/** @override */
veen.schema.Database.prototype.name = function() {
  return 'crdb';
};

/** @override */
veen.schema.Database.prototype.version = function() {
  return 1;
};

/** @override */
veen.schema.Database.prototype.tables = function() {
  return [
    this.imageCache_,
    this.asset_,
    this.pin_,
    this.infoCard_
  ];
};

/** @override */
veen.schema.Database.prototype.info = function() {
  if (!this.metaInfo_) {
    this.metaInfo_ = new lf.schema.Info(this);
  }
  return this.metaInfo_;
};

/** @override */
veen.schema.Database.prototype.table = function(tableName) {
  return this.tableMap_[tableName] || null;
};

/** @override */
veen.schema.Database.prototype.pragma = function() {
  return this.pragma_;
};

/** @return {!veen.schema.ImageCache} */
veen.schema.Database.prototype.getImageCache = function() {
  return this.imageCache_;
};

/** @return {!veen.schema.Asset} */
veen.schema.Database.prototype.getAsset = function() {
  return this.asset_;
};

/** @return {!veen.schema.Pin} */
veen.schema.Database.prototype.getPin = function() {
  return this.pin_;
};

/** @return {!veen.schema.InfoCard} */
veen.schema.Database.prototype.getInfoCard = function() {
  return this.infoCard_;
};

/**
 * @extends {lf.schema.Table.<!veen.row.ImageCacheType,
 *     !veen.row.ImageCacheDbType>}
 * @constructor
 */
veen.schema.ImageCache = function() {
  var cols = [];

  /** @type {!lf.schema.BaseColumn.<string>} */
  this.remote = new lf.schema.BaseColumn(
      this, 'remote', true, false, lf.Type.STRING);
  cols.push(this.remote);

  /** @type {!lf.schema.BaseColumn.<string>} */
  this.local = new lf.schema.BaseColumn(
      this, 'local', false, false, lf.Type.STRING);
  cols.push(this.local);

  var indices = [
    new lf.schema.Index('ImageCache', 'pkImageCache', true,
        [
          {schema: this.remote, order: lf.Order.ASC, autoIncrement: false}
        ])
  ];

  /** @private {!lf.schema.Constraint} */
  this.constraint_;

  veen.schema.ImageCache.base(
      this, 'constructor', 'ImageCache', cols, indices, false);
};
goog.inherits(veen.schema.ImageCache, lf.schema.Table);

/** @override */
veen.schema.ImageCache.prototype.createRow = function(opt_value) {
  return new veen.row.ImageCache(lf.Row.getNextId(), opt_value);
};

/** @override */
veen.schema.ImageCache.prototype.deserializeRow =
    function(dbRecord) {
  var data = dbRecord['value'];
  return new veen.row.ImageCache(dbRecord['id'], data);
};

/** @override */
veen.schema.ImageCache.prototype.getConstraint = function() {
  if (goog.isDefAndNotNull(this.constraint_)) {
    return this.constraint_;
  }

  var pk = this.getIndices()[0];
  var notNullable = [
    this.remote,
    this.local
  ];
  var foreignKeys = [
  ];
  this.constraint_ = new lf.schema.Constraint(
      pk, notNullable, foreignKeys);
  return this.constraint_;
};

/**
 * @export
 * @constructor
 * @struct
 * @final
 */
veen.row.ImageCacheType = function() {
  /** @export @type {string} */
  this.remote;
  /** @export @type {string} */
  this.local;
};

/**
 * @export
 * @constructor
 * @struct
 * @final
 */
veen.row.ImageCacheDbType = function() {
  /** @export @type {string} */
  this.remote;
  /** @export @type {string} */
  this.local;
};

/**
 * Constructs a new ImageCache row.
 * @constructor
 * @extends {lf.Row.<!veen.row.ImageCacheType,
 *     !veen.row.ImageCacheDbType>}
 *
 * @param {number} rowId The row ID.
 * @param {!veen.row.ImageCacheType=} opt_payload
 */
veen.row.ImageCache = function(rowId, opt_payload) {
  veen.row.ImageCache.base(this, 'constructor', rowId, opt_payload);
};
goog.inherits(veen.row.ImageCache, lf.Row);

/** @override */
veen.row.ImageCache.prototype.defaultPayload = function() {
  var payload = new veen.row.ImageCacheType();
  payload.remote = '';
  payload.local = '';
  return payload;
};

/** @override */
veen.row.ImageCache.prototype.toDbPayload = function() {
  var payload = new veen.row.ImageCacheDbType();
  payload.remote = this.payload().remote;
  payload.local = this.payload().local;
  return payload;
};

/** @override */
veen.row.ImageCache.prototype.keyOfIndex = function(indexName) {
  switch (indexName) {
    case 'ImageCache.pkImageCache':
      return this.payload().remote;
    case 'ImageCache.#':
      return this.id();
    default:
      break;
  }
  return null;
};

/** @return {string} */
veen.row.ImageCache.prototype.getRemote = function() {
  return this.payload().remote;
};

/**
 * @param {string} value
 * @return {!veen.row.ImageCache}
*/
veen.row.ImageCache.prototype.setRemote = function(value) {
  this.payload().remote = value;
  return this;
};

/** @return {string} */
veen.row.ImageCache.prototype.getLocal = function() {
  return this.payload().local;
};

/**
 * @param {string} value
 * @return {!veen.row.ImageCache}
*/
veen.row.ImageCache.prototype.setLocal = function(value) {
  this.payload().local = value;
  return this;
};

/**
 * @extends {lf.schema.Table.<!veen.row.AssetType,
 *     !veen.row.AssetDbType>}
 * @constructor
 */
veen.schema.Asset = function() {
  var cols = [];

  /** @type {!lf.schema.BaseColumn.<string>} */
  this.id = new lf.schema.BaseColumn(
      this, 'id', true, false, lf.Type.STRING);
  cols.push(this.id);

  /** @type {!lf.schema.BaseColumn.<string>} */
  this.asset = new lf.schema.BaseColumn(
      this, 'asset', false, false, lf.Type.STRING);
  cols.push(this.asset);

  /** @type {!lf.schema.BaseColumn.<number>} */
  this.timestamp = new lf.schema.BaseColumn(
      this, 'timestamp', false, false, lf.Type.INTEGER);
  cols.push(this.timestamp);

  var indices = [
    new lf.schema.Index('Asset', 'pkAsset', true,
        [
          {schema: this.id, order: lf.Order.ASC, autoIncrement: false}
        ])
  ];

  /** @private {!lf.schema.Constraint} */
  this.constraint_;

  veen.schema.Asset.base(
      this, 'constructor', 'Asset', cols, indices, false);
};
goog.inherits(veen.schema.Asset, lf.schema.Table);

/** @override */
veen.schema.Asset.prototype.createRow = function(opt_value) {
  return new veen.row.Asset(lf.Row.getNextId(), opt_value);
};

/** @override */
veen.schema.Asset.prototype.deserializeRow =
    function(dbRecord) {
  var data = dbRecord['value'];
  return new veen.row.Asset(dbRecord['id'], data);
};

/** @override */
veen.schema.Asset.prototype.getConstraint = function() {
  if (goog.isDefAndNotNull(this.constraint_)) {
    return this.constraint_;
  }

  var pk = this.getIndices()[0];
  var notNullable = [
    this.id,
    this.asset,
    this.timestamp
  ];
  var foreignKeys = [
  ];
  this.constraint_ = new lf.schema.Constraint(
      pk, notNullable, foreignKeys);
  return this.constraint_;
};

/**
 * @export
 * @constructor
 * @struct
 * @final
 */
veen.row.AssetType = function() {
  /** @export @type {string} */
  this.id;
  /** @export @type {string} */
  this.asset;
  /** @export @type {number} */
  this.timestamp;
};

/**
 * @export
 * @constructor
 * @struct
 * @final
 */
veen.row.AssetDbType = function() {
  /** @export @type {string} */
  this.id;
  /** @export @type {string} */
  this.asset;
  /** @export @type {number} */
  this.timestamp;
};

/**
 * Constructs a new Asset row.
 * @constructor
 * @extends {lf.Row.<!veen.row.AssetType,
 *     !veen.row.AssetDbType>}
 *
 * @param {number} rowId The row ID.
 * @param {!veen.row.AssetType=} opt_payload
 */
veen.row.Asset = function(rowId, opt_payload) {
  veen.row.Asset.base(this, 'constructor', rowId, opt_payload);
};
goog.inherits(veen.row.Asset, lf.Row);

/** @override */
veen.row.Asset.prototype.defaultPayload = function() {
  var payload = new veen.row.AssetType();
  payload.id = '';
  payload.asset = '';
  payload.timestamp = 0;
  return payload;
};

/** @override */
veen.row.Asset.prototype.toDbPayload = function() {
  var payload = new veen.row.AssetDbType();
  payload.id = this.payload().id;
  payload.asset = this.payload().asset;
  payload.timestamp = this.payload().timestamp;
  return payload;
};

/** @override */
veen.row.Asset.prototype.keyOfIndex = function(indexName) {
  switch (indexName) {
    case 'Asset.pkAsset':
      return this.payload().id;
    case 'Asset.#':
      return this.id();
    default:
      break;
  }
  return null;
};

/** @return {string} */
veen.row.Asset.prototype.getId = function() {
  return this.payload().id;
};

/**
 * @param {string} value
 * @return {!veen.row.Asset}
*/
veen.row.Asset.prototype.setId = function(value) {
  this.payload().id = value;
  return this;
};

/** @return {string} */
veen.row.Asset.prototype.getAsset = function() {
  return this.payload().asset;
};

/**
 * @param {string} value
 * @return {!veen.row.Asset}
*/
veen.row.Asset.prototype.setAsset = function(value) {
  this.payload().asset = value;
  return this;
};

/** @return {number} */
veen.row.Asset.prototype.getTimestamp = function() {
  return this.payload().timestamp;
};

/**
 * @param {number} value
 * @return {!veen.row.Asset}
*/
veen.row.Asset.prototype.setTimestamp = function(value) {
  this.payload().timestamp = value;
  return this;
};

/**
 * @extends {lf.schema.Table.<!veen.row.PinType,
 *     !veen.row.PinDbType>}
 * @constructor
 */
veen.schema.Pin = function() {
  var cols = [];

  /** @type {!lf.schema.BaseColumn.<string>} */
  this.id = new lf.schema.BaseColumn(
      this, 'id', false, false, lf.Type.STRING);
  cols.push(this.id);

  /** @type {!lf.schema.BaseColumn.<number>} */
  this.state = new lf.schema.BaseColumn(
      this, 'state', false, false, lf.Type.INTEGER);
  cols.push(this.state);

  /** @type {!lf.schema.BaseColumn.<string>} */
  this.sessionId = new lf.schema.BaseColumn(
      this, 'sessionId', false, false, lf.Type.STRING);
  cols.push(this.sessionId);

  var indices = [
    new lf.schema.Index('Pin', 'fkId', false,
        [
          {schema: this.id, order: lf.Order.ASC}
        ])
  ];

  /** @private {!lf.schema.Constraint} */
  this.constraint_;

  veen.schema.Pin.base(
      this, 'constructor', 'Pin', cols, indices, false);
};
goog.inherits(veen.schema.Pin, lf.schema.Table);

/** @override */
veen.schema.Pin.prototype.createRow = function(opt_value) {
  return new veen.row.Pin(lf.Row.getNextId(), opt_value);
};

/** @override */
veen.schema.Pin.prototype.deserializeRow =
    function(dbRecord) {
  var data = dbRecord['value'];
  return new veen.row.Pin(dbRecord['id'], data);
};

/** @override */
veen.schema.Pin.prototype.getConstraint = function() {
  if (goog.isDefAndNotNull(this.constraint_)) {
    return this.constraint_;
  }

  var pk = null;
  var notNullable = [
    this.id,
    this.state,
    this.sessionId
  ];
  var foreignKeys = [
    new lf.schema.ForeignKeySpec(
        {
          'local': 'id',
          'ref': 'Asset.id',
          'action': lf.ConstraintAction.CASCADE,
          'timing': lf.ConstraintTiming.IMMEDIATE
        }, 'Pin', 'fkId')
  ];
  this.constraint_ = new lf.schema.Constraint(
      pk, notNullable, foreignKeys);
  return this.constraint_;
};

/**
 * @export
 * @constructor
 * @struct
 * @final
 */
veen.row.PinType = function() {
  /** @export @type {string} */
  this.id;
  /** @export @type {number} */
  this.state;
  /** @export @type {string} */
  this.sessionId;
};

/**
 * @export
 * @constructor
 * @struct
 * @final
 */
veen.row.PinDbType = function() {
  /** @export @type {string} */
  this.id;
  /** @export @type {number} */
  this.state;
  /** @export @type {string} */
  this.sessionId;
};

/**
 * Constructs a new Pin row.
 * @constructor
 * @extends {lf.Row.<!veen.row.PinType,
 *     !veen.row.PinDbType>}
 *
 * @param {number} rowId The row ID.
 * @param {!veen.row.PinType=} opt_payload
 */
veen.row.Pin = function(rowId, opt_payload) {
  veen.row.Pin.base(this, 'constructor', rowId, opt_payload);
};
goog.inherits(veen.row.Pin, lf.Row);

/** @override */
veen.row.Pin.prototype.defaultPayload = function() {
  var payload = new veen.row.PinType();
  payload.id = '';
  payload.state = 0;
  payload.sessionId = '';
  return payload;
};

/** @override */
veen.row.Pin.prototype.toDbPayload = function() {
  var payload = new veen.row.PinDbType();
  payload.id = this.payload().id;
  payload.state = this.payload().state;
  payload.sessionId = this.payload().sessionId;
  return payload;
};

/** @override */
veen.row.Pin.prototype.keyOfIndex = function(indexName) {
  switch (indexName) {
    case 'Pin.fkId':
      return this.payload().id;
    case 'Pin.#':
      return this.id();
    default:
      break;
  }
  return null;
};

/** @return {string} */
veen.row.Pin.prototype.getId = function() {
  return this.payload().id;
};

/**
 * @param {string} value
 * @return {!veen.row.Pin}
*/
veen.row.Pin.prototype.setId = function(value) {
  this.payload().id = value;
  return this;
};

/** @return {number} */
veen.row.Pin.prototype.getState = function() {
  return this.payload().state;
};

/**
 * @param {number} value
 * @return {!veen.row.Pin}
*/
veen.row.Pin.prototype.setState = function(value) {
  this.payload().state = value;
  return this;
};

/** @return {string} */
veen.row.Pin.prototype.getSessionId = function() {
  return this.payload().sessionId;
};

/**
 * @param {string} value
 * @return {!veen.row.Pin}
*/
veen.row.Pin.prototype.setSessionId = function(value) {
  this.payload().sessionId = value;
  return this;
};

/**
 * @extends {lf.schema.Table.<!veen.row.InfoCardType,
 *     !veen.row.InfoCardDbType>}
 * @constructor
 */
veen.schema.InfoCard = function() {
  var cols = [];

  /** @type {!lf.schema.BaseColumn.<string>} */
  this.id = new lf.schema.BaseColumn(
      this, 'id', false, false, lf.Type.STRING);
  cols.push(this.id);

  /** @type {!lf.schema.BaseColumn.<string>} */
  this.lang = new lf.schema.BaseColumn(
      this, 'lang', false, false, lf.Type.STRING);
  cols.push(this.lang);

  /** @type {!lf.schema.BaseColumn.<number>} */
  this.itag = new lf.schema.BaseColumn(
      this, 'itag', false, false, lf.Type.INTEGER);
  cols.push(this.itag);

  /** @type {!lf.schema.BaseColumn.<string>} */
  this.country = new lf.schema.BaseColumn(
      this, 'country', false, false, lf.Type.STRING);
  cols.push(this.country);

  /** @type {!lf.schema.BaseColumn.<string>} */
  this.fileName = new lf.schema.BaseColumn(
      this, 'fileName', true, false, lf.Type.STRING);
  cols.push(this.fileName);

  var indices = [
    new lf.schema.Index('InfoCard', 'pkInfoCard', true,
        [
          {schema: this.id, order: lf.Order.ASC, autoIncrement: false},
          {schema: this.lang, order: lf.Order.ASC, autoIncrement: false}
        ]),
    new lf.schema.Index('InfoCard', 'uniqFN', true,
        [
          {schema: this.fileName, order: lf.Order.ASC}
        ]),
    new lf.schema.Index('InfoCard', 'idxPinItag', false,
        [
          {schema: this.itag, order: lf.Order.ASC}
        ])
  ];

  /** @private {!lf.schema.Constraint} */
  this.constraint_;

  veen.schema.InfoCard.base(
      this, 'constructor', 'InfoCard', cols, indices, false);
};
goog.inherits(veen.schema.InfoCard, lf.schema.Table);

/** @override */
veen.schema.InfoCard.prototype.createRow = function(opt_value) {
  return new veen.row.InfoCard(lf.Row.getNextId(), opt_value);
};

/** @override */
veen.schema.InfoCard.prototype.deserializeRow =
    function(dbRecord) {
  var data = dbRecord['value'];
  return new veen.row.InfoCard(dbRecord['id'], data);
};

/** @override */
veen.schema.InfoCard.prototype.getConstraint = function() {
  if (goog.isDefAndNotNull(this.constraint_)) {
    return this.constraint_;
  }

  var pk = this.getIndices()[0];
  var notNullable = [
    this.id,
    this.lang,
    this.itag,
    this.country,
    this.fileName
  ];
  var foreignKeys = [
  ];
  this.constraint_ = new lf.schema.Constraint(
      pk, notNullable, foreignKeys);
  return this.constraint_;
};

/**
 * @export
 * @constructor
 * @struct
 * @final
 */
veen.row.InfoCardType = function() {
  /** @export @type {string} */
  this.id;
  /** @export @type {string} */
  this.lang;
  /** @export @type {number} */
  this.itag;
  /** @export @type {string} */
  this.country;
  /** @export @type {string} */
  this.fileName;
};

/**
 * @export
 * @constructor
 * @struct
 * @final
 */
veen.row.InfoCardDbType = function() {
  /** @export @type {string} */
  this.id;
  /** @export @type {string} */
  this.lang;
  /** @export @type {number} */
  this.itag;
  /** @export @type {string} */
  this.country;
  /** @export @type {string} */
  this.fileName;
};

/**
 * Constructs a new InfoCard row.
 * @constructor
 * @extends {lf.Row.<!veen.row.InfoCardType,
 *     !veen.row.InfoCardDbType>}
 *
 * @param {number} rowId The row ID.
 * @param {!veen.row.InfoCardType=} opt_payload
 */
veen.row.InfoCard = function(rowId, opt_payload) {
  veen.row.InfoCard.base(this, 'constructor', rowId, opt_payload);
};
goog.inherits(veen.row.InfoCard, lf.Row);

/** @override */
veen.row.InfoCard.prototype.defaultPayload = function() {
  var payload = new veen.row.InfoCardType();
  payload.id = '';
  payload.lang = '';
  payload.itag = 0;
  payload.country = '';
  payload.fileName = '';
  return payload;
};

/** @override */
veen.row.InfoCard.prototype.toDbPayload = function() {
  var payload = new veen.row.InfoCardDbType();
  payload.id = this.payload().id;
  payload.lang = this.payload().lang;
  payload.itag = this.payload().itag;
  payload.country = this.payload().country;
  payload.fileName = this.payload().fileName;
  return payload;
};

/** @override */
veen.row.InfoCard.prototype.keyOfIndex = function(indexName) {
  switch (indexName) {
    case 'InfoCard.pkInfoCard':
      return [
        this.payload().id,
        this.payload().lang
      ];
    case 'InfoCard.uniqFN':
      return this.payload().fileName;
    case 'InfoCard.idxPinItag':
      return this.payload().itag;
    case 'InfoCard.#':
      return this.id();
    default:
      break;
  }
  return null;
};

/** @return {string} */
veen.row.InfoCard.prototype.getId = function() {
  return this.payload().id;
};

/**
 * @param {string} value
 * @return {!veen.row.InfoCard}
*/
veen.row.InfoCard.prototype.setId = function(value) {
  this.payload().id = value;
  return this;
};

/** @return {string} */
veen.row.InfoCard.prototype.getLang = function() {
  return this.payload().lang;
};

/**
 * @param {string} value
 * @return {!veen.row.InfoCard}
*/
veen.row.InfoCard.prototype.setLang = function(value) {
  this.payload().lang = value;
  return this;
};

/** @return {number} */
veen.row.InfoCard.prototype.getItag = function() {
  return this.payload().itag;
};

/**
 * @param {number} value
 * @return {!veen.row.InfoCard}
*/
veen.row.InfoCard.prototype.setItag = function(value) {
  this.payload().itag = value;
  return this;
};

/** @return {string} */
veen.row.InfoCard.prototype.getCountry = function() {
  return this.payload().country;
};

/**
 * @param {string} value
 * @return {!veen.row.InfoCard}
*/
veen.row.InfoCard.prototype.setCountry = function(value) {
  this.payload().country = value;
  return this;
};

/** @return {string} */
veen.row.InfoCard.prototype.getFileName = function() {
  return this.payload().fileName;
};

/**
 * @param {string} value
 * @return {!veen.row.InfoCard}
*/
veen.row.InfoCard.prototype.setFileName = function(value) {
  this.payload().fileName = value;
  return this;
};
goog.provide('veen');

goog.require('lf.Exception');
goog.require('lf.Global');
/** @suppress {extraRequire} */
goog.require('lf.fn');
/** @suppress {extraRequire} */
goog.require('lf.op');
goog.require('lf.proc.Database');
goog.require('lf.service');
goog.require('lf.service.ServiceId');
goog.require('veen.schema.Database');

/**
 * @return {!lf.Global} The Global instance that refers to veen.
 */
veen.getGlobal = function() {
  var namespacedGlobalId = new lf.service.ServiceId('ns_crdb');
  var global = lf.Global.get();

  var namespacedGlobal = null;
  if (!global.isRegistered(namespacedGlobalId)) {
    namespacedGlobal = new lf.Global();
    global.registerService(namespacedGlobalId, namespacedGlobal);
  } else {
    namespacedGlobal = global.getService(namespacedGlobalId);
  }

  return namespacedGlobal;
};

/** @return {!lf.schema.Database} */
veen.getSchema = function() {
  var global = veen.getGlobal();

  if (!global.isRegistered(lf.service.SCHEMA)) {
    var schema = new veen.schema.Database();
    global.registerService(lf.service.SCHEMA, schema);
  }
  return global.getService(lf.service.SCHEMA);
};

/**
 * Whether a DB connection operation is in progress.
 * @private {boolean}
 */
veen.connectInProgress_ = false;

/**
 * @param {!lf.schema.ConnectOptions=} opt_options
 * @return {!IThenable<!lf.proc.Database>}
 */
veen.connect = function(opt_options) {
  if (veen.connectInProgress_ ||
      (!goog.isNull(veen.db_) && veen.db_.isOpen())) {
    // 113: Attempt to connect() to an already connected/connecting database.
    throw new lf.Exception(113);
  }
  veen.connectInProgress_ = true;

  if (goog.isNull(veen.db_)) {
    veen.getSchema();
    veen.db_ = new lf.proc.Database(veen.getGlobal());
  }

  return veen.db_.init(opt_options).then(
      function(db) {
        veen.connectInProgress_ = false;
        return db;
      },
      function(e) {
        veen.connectInProgress_ = false;
        throw e;
      });
};

/** @private {?lf.proc.Database} */
veen.db_ = null;
bimusiek commented 7 years ago

This file is the base one, then you can see that I am using google closure compiler to compile this file together with lovefield sources into one file without goog.

#!/usr/bin/env bash
SCRIPTPATH=$(dirname $BASH_SOURCE)
# Here we will generate js version of schema
${SCRIPTPATH}/spac.sh --schema ${SCRIPTPATH}/../web/app/libs/database/schema.yml --outputdir ${SCRIPTPATH}/../web/app/libs/database/lovefield/ --namespace ahoydb
# and here we take this file that has `goog` and compile it together with lovefield to make 1 file that has both lovefield and your database schema
java -jar ${SCRIPTPATH}/../tools/lovefield/node_modules/google-closure-compiler/compiler.jar --dependency_mode=STRICT --entry_point='ahoydb.row.FlightBooking' --js_output_file=${SCRIPTPATH}/../web/app/libs/database/ahoydb.js "${SCRIPTPATH}/../tools/lovefield/node_modules/google-closure-library/**.js" '!**_test.js' "${SCRIPTPATH}/../tools/lovefield/lib/**.js" "${SCRIPTPATH}/../web/app/libs/database/lovefield/**.js"